@chipschat/hermes 1.0.7 → 1.0.8
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 +38 -6
- package/dist/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/main.js +1731 -1316
- 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 readFileSync11, 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 = {};
|
|
@@ -25555,7 +25555,7 @@ function fileStore(path) {
|
|
|
25555
25555
|
|
|
25556
25556
|
// src/bridge.ts
|
|
25557
25557
|
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
25558
|
-
import { join as
|
|
25558
|
+
import { join as join10 } from "node:path";
|
|
25559
25559
|
|
|
25560
25560
|
// src/context.ts
|
|
25561
25561
|
import { spawn } from "node:child_process";
|
|
@@ -25578,7 +25578,7 @@ import { basename as basename3, dirname as dirname4, join as join6 } from "node:
|
|
|
25578
25578
|
|
|
25579
25579
|
// src/home.ts
|
|
25580
25580
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
25581
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
|
|
25581
|
+
import { appendFileSync, chmodSync as chmodSync3, existsSync as existsSync5, readFileSync as readFileSync5, readlinkSync, statSync } from "node:fs";
|
|
25582
25582
|
import { homedir as homedir4 } from "node:os";
|
|
25583
25583
|
import { join as join5, resolve as resolve2 } from "node:path";
|
|
25584
25584
|
function hermesRoot(env = process.env) {
|
|
@@ -25605,6 +25605,14 @@ function envValue(home2, name2) {
|
|
|
25605
25605
|
}
|
|
25606
25606
|
return found;
|
|
25607
25607
|
}
|
|
25608
|
+
function appendEnv(home2, name2, value) {
|
|
25609
|
+
const path = join5(home2, ".env");
|
|
25610
|
+
const text = readFileSync5(path, "utf8");
|
|
25611
|
+
const mode = statSync(path).mode & 511;
|
|
25612
|
+
appendFileSync(path, `${text === "" || text.endsWith("\n") ? "" : "\n"}${name2}=${value}
|
|
25613
|
+
`);
|
|
25614
|
+
chmodSync3(path, mode);
|
|
25615
|
+
}
|
|
25608
25616
|
function configText(home2) {
|
|
25609
25617
|
const path = join5(home2, "config.yaml");
|
|
25610
25618
|
try {
|
|
@@ -25839,13 +25847,9 @@ function fileOf(source2, name2) {
|
|
|
25839
25847
|
}
|
|
25840
25848
|
var byOrder = (a, b) => ORDER.indexOf(a.kind) - ORDER.indexOf(b.kind) || a.profile.localeCompare(b.profile);
|
|
25841
25849
|
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) {
|
|
25850
|
+
constructor(readers, first, env = process.env, ownProfiles = false) {
|
|
25851
|
+
this.first = first;
|
|
25852
|
+
this.ownProfiles = ownProfiles;
|
|
25849
25853
|
const byProfile = /* @__PURE__ */ new Map();
|
|
25850
25854
|
const sourcesFor = (profile) => {
|
|
25851
25855
|
let sources = byProfile.get(profile.home);
|
|
@@ -25867,7 +25871,7 @@ var Picture = class {
|
|
|
25867
25871
|
}
|
|
25868
25872
|
const taken = /* @__PURE__ */ new Set();
|
|
25869
25873
|
const ids2 = /* @__PURE__ */ new Set();
|
|
25870
|
-
const sharing = [...this.readers.values()].filter(({ source: source2, agents }) => source2.kind === "project" || source2
|
|
25874
|
+
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
25875
|
for (const { source: source2 } of sharing) {
|
|
25872
25876
|
const name2 = taken.has(source2.file) ? `${source2.file} (${source2.profile})` : source2.file;
|
|
25873
25877
|
taken.add(name2);
|
|
@@ -25877,8 +25881,24 @@ var Picture = class {
|
|
|
25877
25881
|
this.shared.set(source2.path, { id: id2, name: name2 });
|
|
25878
25882
|
}
|
|
25879
25883
|
}
|
|
25884
|
+
first;
|
|
25885
|
+
ownProfiles;
|
|
25886
|
+
/** Each file by its path, and the agents that read it. */
|
|
25887
|
+
readers = /* @__PURE__ */ new Map();
|
|
25888
|
+
/** The shared files, each with its id and the name the set shows. */
|
|
25889
|
+
shared = /* @__PURE__ */ new Map();
|
|
25890
|
+
/** The agents and the files each reads, in page order. */
|
|
25891
|
+
agents = /* @__PURE__ */ new Map();
|
|
25892
|
+
/**
|
|
25893
|
+
* Whether each new agent starts with this file or a copy of it: the first profile's files, or
|
|
25894
|
+
* where each new agent gets a profile of its own, the first profile's SOUL.md and the project file.
|
|
25895
|
+
*/
|
|
25896
|
+
forNewAgents(source2) {
|
|
25897
|
+
if (source2.profile !== this.first?.name) return false;
|
|
25898
|
+
return !this.ownProfiles || source2.kind === "soul" || source2.kind === "project";
|
|
25899
|
+
}
|
|
25880
25900
|
/** The harness's own set, as `PUT /v1/harnesses/:id/context` takes it. */
|
|
25881
|
-
harnessCopy(
|
|
25901
|
+
harnessCopy() {
|
|
25882
25902
|
const shown2 = [];
|
|
25883
25903
|
const notShown = [];
|
|
25884
25904
|
for (const [path, { id: id2, name: name2 }] of this.shared) {
|
|
@@ -25894,8 +25914,7 @@ var Picture = class {
|
|
|
25894
25914
|
...file3,
|
|
25895
25915
|
id: id2,
|
|
25896
25916
|
used_by: agents.length,
|
|
25897
|
-
|
|
25898
|
-
...source2.profile === first?.name ? { for_new_agents: true } : {}
|
|
25917
|
+
...this.forNewAgents(source2) ? { for_new_agents: true } : {}
|
|
25899
25918
|
});
|
|
25900
25919
|
}
|
|
25901
25920
|
return {
|
|
@@ -26056,9 +26075,9 @@ var Contexts = class {
|
|
|
26056
26075
|
/** Every copy read again, and sent where it moved. */
|
|
26057
26076
|
refresh() {
|
|
26058
26077
|
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(
|
|
26078
|
+
const { readers, first, ownProfiles } = this.roster();
|
|
26079
|
+
const picture = new Picture(readers, first, this.env, ownProfiles);
|
|
26080
|
+
await this.port.harness(picture.harnessCopy()).catch((error62) => {
|
|
26062
26081
|
this.log(`the harness's context not sent: ${messageOf(error62)}`);
|
|
26063
26082
|
});
|
|
26064
26083
|
for (const { agentId: agentId2 } of readers) {
|
|
@@ -26076,12 +26095,12 @@ var Contexts = class {
|
|
|
26076
26095
|
*/
|
|
26077
26096
|
async edit(frame) {
|
|
26078
26097
|
const answer = await this.inLine(async () => {
|
|
26079
|
-
const { readers, first } = this.roster();
|
|
26098
|
+
const { readers, first, ownProfiles } = this.roster();
|
|
26080
26099
|
const copyNow = () => {
|
|
26081
|
-
const picture = new Picture(readers, first, this.env);
|
|
26082
|
-
return frame.agent_id === void 0 ? picture.harnessCopy(
|
|
26100
|
+
const picture = new Picture(readers, first, this.env, ownProfiles);
|
|
26101
|
+
return frame.agent_id === void 0 ? picture.harnessCopy() : picture.agentCopy(frame.agent_id);
|
|
26083
26102
|
};
|
|
26084
|
-
const found = new Picture(readers, first, this.env).find(frame.agent_id, frame.name);
|
|
26103
|
+
const found = new Picture(readers, first, this.env, ownProfiles).find(frame.agent_id, frame.name);
|
|
26085
26104
|
if (found === void 0) {
|
|
26086
26105
|
throw new Refuse(`Hermes gives this agent no file called ${frame.name}.`, copyNow(), "failed");
|
|
26087
26106
|
}
|
|
@@ -26148,6 +26167,8 @@ function numbersOf(profile, body) {
|
|
|
26148
26167
|
if (typeof body !== "object" || body === null) return void 0;
|
|
26149
26168
|
const top = body;
|
|
26150
26169
|
if (top["hook_event_name"] !== "post_api_request") return void 0;
|
|
26170
|
+
const sender = top["profile"];
|
|
26171
|
+
if (typeof sender === "string" && sender !== profile && sender !== "custom") return void 0;
|
|
26151
26172
|
const extra = typeof top["extra"] === "object" && top["extra"] !== null ? top["extra"] : {};
|
|
26152
26173
|
const named = typeof top["session_id"] === "string" ? top["session_id"] : "";
|
|
26153
26174
|
const task2 = typeof extra["task_id"] === "string" && extra["task_id"] !== "" ? extra["task_id"] : void 0;
|
|
@@ -26453,6 +26474,13 @@ var HermesClient = class {
|
|
|
26453
26474
|
const latest = read["session_id"];
|
|
26454
26475
|
return typeof latest === "string" && latest !== "" ? latest : sessionId;
|
|
26455
26476
|
}
|
|
26477
|
+
/**
|
|
26478
|
+
* The models Hermes has for this profile, as its own picker lists them: `GET /api/model/options`,
|
|
26479
|
+
* only the providers the profile is set up for (`models.ts`).
|
|
26480
|
+
*/
|
|
26481
|
+
async modelOptions() {
|
|
26482
|
+
return await this.call("GET", "/api/model/options?include_unconfigured=false");
|
|
26483
|
+
}
|
|
26456
26484
|
/** A run in a session: what it is told, and under a key a retry of the same turn repeats. */
|
|
26457
26485
|
async startRun(input2) {
|
|
26458
26486
|
const started = await this.call(
|
|
@@ -26462,6 +26490,7 @@ var HermesClient = class {
|
|
|
26462
26490
|
input: input2.text,
|
|
26463
26491
|
session_id: input2.sessionId,
|
|
26464
26492
|
...input2.model === void 0 ? {} : { model: input2.model },
|
|
26493
|
+
...input2.provider === void 0 ? {} : { provider: input2.provider },
|
|
26465
26494
|
...input2.instructions === void 0 ? {} : { instructions: input2.instructions }
|
|
26466
26495
|
},
|
|
26467
26496
|
{ "Idempotency-Key": input2.key.slice(0, 255) }
|
|
@@ -26558,7 +26587,7 @@ async function failureOf(answer) {
|
|
|
26558
26587
|
}
|
|
26559
26588
|
|
|
26560
26589
|
// src/hooks.ts
|
|
26561
|
-
import {
|
|
26590
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
|
|
26562
26591
|
import { join as join7 } from "node:path";
|
|
26563
26592
|
var HOOK_NAME = "chips";
|
|
26564
26593
|
var SECRET_ENV = "CHIPS_HOOK_SECRET";
|
|
@@ -26595,12 +26624,7 @@ function entryLines(indent, url2, secret) {
|
|
|
26595
26624
|
function secretInEnv(home2, secret) {
|
|
26596
26625
|
const path = join7(home2, ".env");
|
|
26597
26626
|
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);
|
|
26627
|
+
if (envValue(home2, SECRET_ENV) === void 0) appendEnv(home2, SECRET_ENV, secret);
|
|
26604
26628
|
return true;
|
|
26605
26629
|
}
|
|
26606
26630
|
function addHook(home2, port, profile, secret) {
|
|
@@ -26661,1417 +26685,1804 @@ function addHook(home2, port, profile, secret) {
|
|
|
26661
26685
|
return { state: "added", path };
|
|
26662
26686
|
}
|
|
26663
26687
|
|
|
26664
|
-
// src/
|
|
26665
|
-
var
|
|
26666
|
-
|
|
26667
|
-
|
|
26668
|
-
|
|
26669
|
-
|
|
26670
|
-
|
|
26671
|
-
|
|
26688
|
+
// src/models.ts
|
|
26689
|
+
var MODEL_CHOICES_MAX2 = 40;
|
|
26690
|
+
var LABEL_MAX = 80;
|
|
26691
|
+
var DEFAULT_LABEL = "Hermes's default";
|
|
26692
|
+
function choiceId(provider, model) {
|
|
26693
|
+
return `${provider}/${model}`;
|
|
26694
|
+
}
|
|
26695
|
+
function runModelOf(id2) {
|
|
26696
|
+
const at = id2.indexOf("/");
|
|
26697
|
+
return at <= 0 ? { model: id2 } : { provider: id2.slice(0, at), model: id2.slice(at + 1) };
|
|
26698
|
+
}
|
|
26699
|
+
function providersOf(options) {
|
|
26700
|
+
const ready = (options.providers ?? []).filter(
|
|
26701
|
+
(provider) => provider.authenticated !== false && (provider.models ?? []).length > 0
|
|
26702
|
+
);
|
|
26703
|
+
const current = (provider) => provider.is_current === true || provider.slug === options.provider;
|
|
26704
|
+
return [...ready.filter(current), ...ready.filter((provider) => !current(provider))];
|
|
26705
|
+
}
|
|
26706
|
+
function choicesOf(options) {
|
|
26707
|
+
const choices = [];
|
|
26708
|
+
for (const provider of providersOf(options)) {
|
|
26709
|
+
for (const model of provider.models ?? []) {
|
|
26710
|
+
const id2 = choiceId(provider.slug, model);
|
|
26711
|
+
if (choices.some((choice) => choice.id === id2)) continue;
|
|
26712
|
+
choices.push({ id: id2, label: `${model} \xB7 ${provider.name ?? provider.slug}`.slice(0, LABEL_MAX), efforts: [] });
|
|
26713
|
+
}
|
|
26714
|
+
}
|
|
26715
|
+
return choices;
|
|
26716
|
+
}
|
|
26717
|
+
function defaultOf(options) {
|
|
26718
|
+
const model = options.model?.trim();
|
|
26719
|
+
if (model === void 0 || model === "") return void 0;
|
|
26720
|
+
return options.provider === void 0 || options.provider === "" ? model : choiceId(options.provider, model);
|
|
26721
|
+
}
|
|
26722
|
+
function matched(choices, model) {
|
|
26723
|
+
if (choices.some((choice) => choice.id === model)) return model;
|
|
26724
|
+
return choices.find((choice) => runModelOf(choice.id).model === model)?.id ?? model;
|
|
26725
|
+
}
|
|
26726
|
+
function copyOf(options, own2) {
|
|
26727
|
+
let choices = choicesOf(options);
|
|
26728
|
+
const fallback = defaultOf(options);
|
|
26729
|
+
const byDefault = fallback === void 0 ? void 0 : matched(choices, fallback);
|
|
26730
|
+
const mine = own2 === void 0 ? void 0 : matched(choices, own2);
|
|
26731
|
+
const model = mine ?? byDefault ?? choices[0]?.id;
|
|
26732
|
+
if (model === void 0) return void 0;
|
|
26733
|
+
const keep = [model, ...byDefault === void 0 ? [] : [byDefault]];
|
|
26734
|
+
for (const id2 of keep) {
|
|
26735
|
+
if (!choices.some((choice) => choice.id === id2)) choices = [{ id: id2, label: id2.slice(0, LABEL_MAX), efforts: [] }, ...choices];
|
|
26736
|
+
}
|
|
26737
|
+
let others = MODEL_CHOICES_MAX2 - keep.filter((id2, at) => keep.indexOf(id2) === at).length;
|
|
26738
|
+
choices = choices.filter((choice) => keep.includes(choice.id) || others-- > 0);
|
|
26672
26739
|
return {
|
|
26673
|
-
|
|
26674
|
-
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
26740
|
+
model,
|
|
26741
|
+
own: mine !== void 0,
|
|
26742
|
+
...byDefault === void 0 ? {} : { default: { label: DEFAULT_LABEL, model: byDefault } },
|
|
26743
|
+
choices,
|
|
26744
|
+
editable: true
|
|
26678
26745
|
};
|
|
26679
26746
|
}
|
|
26680
|
-
|
|
26681
|
-
|
|
26747
|
+
|
|
26748
|
+
// src/profiles.ts
|
|
26749
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
26750
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, rmSync as rmSync4 } from "node:fs";
|
|
26751
|
+
import { createConnection } from "node:net";
|
|
26752
|
+
import { join as join9 } from "node:path";
|
|
26753
|
+
|
|
26754
|
+
// src/gateway.ts
|
|
26755
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
26756
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
26757
|
+
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";
|
|
26758
|
+
import { homedir as homedir5 } from "node:os";
|
|
26759
|
+
import { basename as basename4, delimiter as delimiter2, dirname as dirname5, join as join8, resolve as resolve3 } from "node:path";
|
|
26760
|
+
var TIMING = {
|
|
26761
|
+
stopMs: 15e3,
|
|
26762
|
+
killMs: 5e3,
|
|
26763
|
+
earlyMs: 1e4,
|
|
26764
|
+
upMs: 9e4,
|
|
26765
|
+
serviceMs: 15e4,
|
|
26766
|
+
pollMs: 500
|
|
26767
|
+
};
|
|
26768
|
+
var EXPAT = "/opt/homebrew/opt/expat/lib/libexpat.1.dylib";
|
|
26769
|
+
var EXPAT_SYMBOL = "_XML_SetAllocTrackerActivationThreshold";
|
|
26770
|
+
function gatewaysOf(agents) {
|
|
26771
|
+
return [...new Set(agents.map(({ address }) => address.url.replace(/\/+$/, "")))].map((url2) => ({ url: url2 }));
|
|
26682
26772
|
}
|
|
26683
|
-
|
|
26684
|
-
|
|
26685
|
-
|
|
26686
|
-
|
|
26773
|
+
function isLocal(url2) {
|
|
26774
|
+
try {
|
|
26775
|
+
return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(new URL(url2).hostname);
|
|
26776
|
+
} catch {
|
|
26777
|
+
return false;
|
|
26778
|
+
}
|
|
26687
26779
|
}
|
|
26688
|
-
function
|
|
26689
|
-
const
|
|
26690
|
-
|
|
26691
|
-
|
|
26692
|
-
|
|
26780
|
+
function portOf(url2) {
|
|
26781
|
+
const parsed = new URL(url2);
|
|
26782
|
+
return Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
|
|
26783
|
+
}
|
|
26784
|
+
function line(words4) {
|
|
26785
|
+
const one = words4.replace(/\s+/g, " ").trim();
|
|
26786
|
+
return one.length <= 200 ? one : `${one.slice(0, 199)}\u2026`;
|
|
26787
|
+
}
|
|
26788
|
+
function whyFrom(output2, code) {
|
|
26789
|
+
const lines2 = output2.split("\n").map((each) => each.trim()).filter((each) => each !== "");
|
|
26790
|
+
const why = lines2.findLast((each) => /^[A-Za-z_][\w.]*(Error|Exception)\b/.test(each)) ?? lines2.at(-1);
|
|
26791
|
+
if (why === void 0) return code === null || code === void 0 ? "it was stopped" : `it exited ${code}`;
|
|
26792
|
+
return why.length <= 200 ? why : why.replace(/\/(?:[^\s/(),:'"]+\/)+([^\s/(),:'"]+)/g, "$1");
|
|
26793
|
+
}
|
|
26794
|
+
function gatewayRunArgs(command) {
|
|
26795
|
+
const words4 = command.trim().split(/\s+/);
|
|
26796
|
+
const at = words4.findIndex((word) => basename4(word) === "hermes");
|
|
26797
|
+
if (at === -1) return void 0;
|
|
26798
|
+
const args = words4.slice(at + 1);
|
|
26799
|
+
const plain = [];
|
|
26800
|
+
for (let next = 0; next < args.length; next += 1) {
|
|
26801
|
+
const word = args[next] ?? "";
|
|
26802
|
+
if (word === "-p" || word === "--profile") next += 1;
|
|
26803
|
+
else if (!word.startsWith("-")) plain.push(word);
|
|
26693
26804
|
}
|
|
26805
|
+
return plain[0] === "gateway" && plain[1] === "run" ? args : void 0;
|
|
26694
26806
|
}
|
|
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, "-")}`;
|
|
26807
|
+
function serviceOf(system, root2, env = {}) {
|
|
26808
|
+
const home2 = resolve3(root2);
|
|
26809
|
+
const native = resolve3(system.home, ".hermes");
|
|
26810
|
+
const suffix = home2 === native ? "" : dirname5(home2) === join8(native, "profiles") ? basename4(home2) : createHash3("sha256").update(home2).digest("hex").slice(0, 8);
|
|
26811
|
+
if (system.platform === "darwin") {
|
|
26812
|
+
const path = join8(system.home, "Library", "LaunchAgents", `ai.hermes.gateway${suffix ? `-${suffix}` : ""}.plist`);
|
|
26813
|
+
return system.exists(path) ? { kind: "launchd", path } : void 0;
|
|
26814
|
+
}
|
|
26815
|
+
if (system.platform === "linux") {
|
|
26816
|
+
const unit = `hermes-gateway${suffix ? `-${suffix}` : ""}.service`;
|
|
26817
|
+
const user = join8(env["XDG_CONFIG_HOME"] || join8(system.home, ".config"), "systemd", "user", unit);
|
|
26818
|
+
if (system.exists(user)) return { kind: "systemd", path: user };
|
|
26819
|
+
const machine = join8("/etc/systemd/system", unit);
|
|
26820
|
+
if (system.exists(machine)) return { kind: "systemd", path: machine, system: true };
|
|
26821
|
+
}
|
|
26822
|
+
return void 0;
|
|
26717
26823
|
}
|
|
26718
|
-
|
|
26824
|
+
function expand(text, env) {
|
|
26825
|
+
let out = "";
|
|
26826
|
+
let at = 0;
|
|
26827
|
+
while (at < text.length) {
|
|
26828
|
+
const char = text[at] ?? "";
|
|
26829
|
+
if (char === "\\" && at + 1 < text.length) {
|
|
26830
|
+
out += text[at + 1];
|
|
26831
|
+
at += 2;
|
|
26832
|
+
} else if (char !== "$") {
|
|
26833
|
+
out += char;
|
|
26834
|
+
at += 1;
|
|
26835
|
+
} else if (text[at + 1] === "{") {
|
|
26836
|
+
let depth = 1;
|
|
26837
|
+
let end2 = at + 2;
|
|
26838
|
+
for (; end2 < text.length; end2 += 1) {
|
|
26839
|
+
if (text[end2] === "{") depth += 1;
|
|
26840
|
+
else if (text[end2] === "}" && --depth === 0) break;
|
|
26841
|
+
}
|
|
26842
|
+
const inner = /^([A-Za-z_][A-Za-z0-9_]*)(?:(:?)-([\s\S]*))?$/.exec(text.slice(at + 2, end2));
|
|
26843
|
+
if (inner !== null) {
|
|
26844
|
+
const value = env[inner[1] ?? ""];
|
|
26845
|
+
const unset = inner[2] === ":" ? value === void 0 || value === "" : value === void 0;
|
|
26846
|
+
out += inner[3] !== void 0 && unset ? expand(inner[3], env) : value ?? "";
|
|
26847
|
+
}
|
|
26848
|
+
at = end2 + 1;
|
|
26849
|
+
} else {
|
|
26850
|
+
const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(at + 1))?.[0];
|
|
26851
|
+
out += name2 === void 0 ? "$" : env[name2] ?? "";
|
|
26852
|
+
at += 1 + (name2?.length ?? 0);
|
|
26853
|
+
}
|
|
26854
|
+
}
|
|
26855
|
+
return out;
|
|
26856
|
+
}
|
|
26857
|
+
function wrapperOf(text, env) {
|
|
26858
|
+
for (const each of text.split("\n")) {
|
|
26859
|
+
const found = /^\s*((?:[A-Za-z_][A-Za-z0-9_]*="(?:[^"\\]|\\.)*"\s+)*)exec\s+"([^"]+)"/.exec(each);
|
|
26860
|
+
if (found === null) continue;
|
|
26861
|
+
const vars = {};
|
|
26862
|
+
for (const set2 of (found[1] ?? "").matchAll(/([A-Za-z_][A-Za-z0-9_]*)="((?:[^"\\]|\\.)*)"/g)) {
|
|
26863
|
+
vars[set2[1] ?? ""] = expand(set2[2] ?? "", env);
|
|
26864
|
+
}
|
|
26865
|
+
return { entry: expand(found[2] ?? "", env), vars };
|
|
26866
|
+
}
|
|
26867
|
+
return void 0;
|
|
26868
|
+
}
|
|
26869
|
+
var NO_HERMES = "The hermes command isn't on this machine's PATH, so Hermes's gateway can't be started.";
|
|
26870
|
+
var messageOf2 = (error62) => error62 instanceof Error ? error62.message : String(error62);
|
|
26871
|
+
function gatewayEnv(env) {
|
|
26872
|
+
return Object.fromEntries(Object.entries(env).filter(([name2]) => !name2.startsWith("CHIPS_")));
|
|
26873
|
+
}
|
|
26874
|
+
async function runHermes(system, env, args, timeoutMs, log2) {
|
|
26875
|
+
const hermes = system.which("hermes", env);
|
|
26876
|
+
if (hermes === void 0) return { code: null, output: "The hermes command isn't on this machine's PATH." };
|
|
26877
|
+
const plain = gatewayEnv(env);
|
|
26878
|
+
const ran = await system.run(hermes, args, plain, timeoutMs);
|
|
26879
|
+
if (ran.code === 0 || !ran.output.includes(EXPAT_SYMBOL) || !system.exists(EXPAT)) return ran;
|
|
26880
|
+
const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
|
|
26881
|
+
if (wrapper === void 0) return ran;
|
|
26882
|
+
const expat = dirname5(EXPAT);
|
|
26883
|
+
log2(
|
|
26884
|
+
`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}.`
|
|
26885
|
+
);
|
|
26886
|
+
const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
|
|
26887
|
+
return system.run(wrapper.entry, args, { ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries }, timeoutMs);
|
|
26888
|
+
}
|
|
26889
|
+
var Gateways = class {
|
|
26719
26890
|
constructor(options) {
|
|
26720
26891
|
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
26892
|
}
|
|
26749
26893
|
options;
|
|
26750
|
-
|
|
26751
|
-
|
|
26752
|
-
|
|
26753
|
-
|
|
26754
|
-
|
|
26755
|
-
|
|
26756
|
-
|
|
26757
|
-
|
|
26758
|
-
|
|
26759
|
-
|
|
26760
|
-
|
|
26761
|
-
|
|
26762
|
-
|
|
26763
|
-
|
|
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 });
|
|
26785
|
-
}
|
|
26786
|
-
this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
|
|
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();
|
|
26894
|
+
timing = TIMING;
|
|
26895
|
+
/** Each gateway restarted, one after another: what went wrong, in a line, or nothing. */
|
|
26896
|
+
async restart() {
|
|
26897
|
+
const troubles = [];
|
|
26898
|
+
for (const gateway of this.options.gateways) {
|
|
26899
|
+
let trouble;
|
|
26900
|
+
try {
|
|
26901
|
+
trouble = await this.restartOne(gateway);
|
|
26902
|
+
} catch (error62) {
|
|
26903
|
+
trouble = `Restart failed: ${messageOf2(error62)}`;
|
|
26904
|
+
}
|
|
26905
|
+
if (trouble !== void 0) troubles.push(trouble);
|
|
26906
|
+
}
|
|
26907
|
+
return troubles.length === 0 ? void 0 : line(troubles.join(" "));
|
|
26803
26908
|
}
|
|
26804
|
-
/**
|
|
26805
|
-
|
|
26806
|
-
|
|
26909
|
+
/** Each local gateway that doesn't answer, started: the bridge is starting. */
|
|
26910
|
+
async startIfDown() {
|
|
26911
|
+
const { system, log: log2 } = this.options;
|
|
26912
|
+
for (const gateway of this.options.gateways) {
|
|
26913
|
+
if (!isLocal(gateway.url)) continue;
|
|
26914
|
+
const port = portOf(gateway.url);
|
|
26915
|
+
try {
|
|
26916
|
+
if (await system.healthy(gateway.url)) continue;
|
|
26917
|
+
let trouble;
|
|
26918
|
+
if (this.serviceFor(gateway) !== void 0) {
|
|
26919
|
+
log2(`Hermes isn't answering on port ${port}: starting its service, hermes gateway start`);
|
|
26920
|
+
trouble = await this.byService(gateway, "start", void 0);
|
|
26921
|
+
} else {
|
|
26922
|
+
const pid = await system.listener(port);
|
|
26923
|
+
if (pid !== void 0) {
|
|
26924
|
+
log2(`Hermes isn't answering on port ${port}, but pid ${pid} listens there: no second gateway started`);
|
|
26925
|
+
continue;
|
|
26926
|
+
}
|
|
26927
|
+
log2(`Hermes isn't answering on port ${port}: starting hermes gateway run`);
|
|
26928
|
+
trouble = await this.start(gateway, ["gateway", "run"], void 0);
|
|
26929
|
+
}
|
|
26930
|
+
if (trouble !== void 0) log2(trouble);
|
|
26931
|
+
} catch (error62) {
|
|
26932
|
+
log2(`Hermes's gateway on port ${port} couldn't be started: ${messageOf2(error62)}`);
|
|
26933
|
+
}
|
|
26934
|
+
}
|
|
26807
26935
|
}
|
|
26808
26936
|
/**
|
|
26809
|
-
*
|
|
26810
|
-
*
|
|
26937
|
+
* The service of Hermes's root, for the Main agent's gateway: a gateway at another address is
|
|
26938
|
+
* another profile's own, which `hermes gateway status` of the root doesn't speak for.
|
|
26811
26939
|
*/
|
|
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();
|
|
26940
|
+
serviceFor(gateway) {
|
|
26941
|
+
if (gateway !== this.options.gateways[0]) return void 0;
|
|
26942
|
+
return serviceOf(this.options.system, this.options.root, this.options.env);
|
|
26822
26943
|
}
|
|
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`);
|
|
26944
|
+
async restartOne(gateway) {
|
|
26945
|
+
const { system, log: log2 } = this.options;
|
|
26946
|
+
if (!isLocal(gateway.url)) {
|
|
26947
|
+
return `Hermes's gateway at ${new URL(gateway.url).host} isn't on this machine, so it can't be restarted from here.`;
|
|
26846
26948
|
}
|
|
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);
|
|
26949
|
+
const port = portOf(gateway.url);
|
|
26950
|
+
const pid = await system.listener(port);
|
|
26951
|
+
if (this.serviceFor(gateway) !== void 0) {
|
|
26952
|
+
log2("restarting Hermes's service: hermes gateway restart");
|
|
26953
|
+
return this.byService(gateway, "restart", pid);
|
|
26863
26954
|
}
|
|
26864
|
-
|
|
26865
|
-
|
|
26866
|
-
|
|
26867
|
-
|
|
26868
|
-
return this.listed.filter((agent) => agent.state === "active");
|
|
26869
|
-
}
|
|
26870
|
-
/** A profile as the context needs it: its name and its home under Hermes's root. */
|
|
26871
|
-
profileOfRunning(agent) {
|
|
26872
|
-
const root2 = this.options.hermesRoot;
|
|
26873
|
-
if (root2 === void 0) return void 0;
|
|
26874
|
-
return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
|
|
26875
|
-
}
|
|
26876
|
-
/** The profile an agent runs on: its own, a copy's original's, else the first (as `profileOf` picks). */
|
|
26877
|
-
profileFor(agentId2) {
|
|
26878
|
-
const own2 = this.agents.find((agent) => agent.agentId === agentId2);
|
|
26879
|
-
const copied = this.options.state.profiles?.[agentId2];
|
|
26880
|
-
const original = this.agents.find((agent) => copied !== void 0 && agent.key === copied);
|
|
26881
|
-
const running = own2 ?? original ?? this.agents[0];
|
|
26882
|
-
return running === void 0 ? void 0 : this.profileOfRunning(running);
|
|
26883
|
-
}
|
|
26884
|
-
/** Who reads what: each active agent with its profile, and the first profile, which new agents run on. */
|
|
26885
|
-
roster() {
|
|
26886
|
-
const readers = [];
|
|
26887
|
-
const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
|
|
26888
|
-
for (const agentId2 of ids2) {
|
|
26889
|
-
if (this.listed.some((agent) => agent.agentId === agentId2 && agent.state !== "active")) continue;
|
|
26890
|
-
const profile = this.profileFor(agentId2);
|
|
26891
|
-
if (profile !== void 0) readers.push({ agentId: agentId2, profile });
|
|
26955
|
+
if (pid === void 0) {
|
|
26956
|
+
if (await system.healthy(gateway.url)) return `Can't tell what answers on port ${port}, so nothing was restarted.`;
|
|
26957
|
+
log2(`nothing listens on port ${port}: starting hermes gateway run`);
|
|
26958
|
+
return this.start(gateway, ["gateway", "run"], void 0);
|
|
26892
26959
|
}
|
|
26893
|
-
const
|
|
26894
|
-
|
|
26895
|
-
|
|
26896
|
-
|
|
26897
|
-
|
|
26898
|
-
|
|
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 };
|
|
26904
|
-
}
|
|
26905
|
-
/** A person's edit of a context file from Chips (Chips ADR 0134), saved and answered. */
|
|
26906
|
-
async onContextEdit(frame) {
|
|
26907
|
-
if (this.contexts === void 0) throw new Refuse("This bridge reads no context files.");
|
|
26908
|
-
if (frame.agent_id !== void 0 && !this.listed.some((agent) => agent.agentId === frame.agent_id)) {
|
|
26909
|
-
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
26960
|
+
const info = await system.describe(pid);
|
|
26961
|
+
if (info === void 0) return this.start(gateway, ["gateway", "run"], void 0);
|
|
26962
|
+
const args = this.ours(info);
|
|
26963
|
+
if (args === void 0) {
|
|
26964
|
+
log2(`pid ${pid} on port ${port} isn't this user's hermes gateway run, so it was left alone: ${info.command.slice(0, 160)}`);
|
|
26965
|
+
return `Port ${port} is held by something other than hermes gateway run (pid ${pid}), so nothing was restarted.`;
|
|
26910
26966
|
}
|
|
26911
|
-
|
|
26912
|
-
|
|
26913
|
-
|
|
26914
|
-
|
|
26915
|
-
throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
|
|
26967
|
+
if (system.which("hermes", this.options.env) === void 0) return NO_HERMES;
|
|
26968
|
+
log2(`stopping Hermes's gateway, pid ${pid}`);
|
|
26969
|
+
if (!await this.stop(pid)) return `Hermes's gateway (pid ${pid}) didn't stop, so it wasn't started again.`;
|
|
26970
|
+
return this.start(gateway, args, info.cwd);
|
|
26916
26971
|
}
|
|
26917
|
-
/** A
|
|
26918
|
-
|
|
26919
|
-
|
|
26920
|
-
throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
|
|
26972
|
+
/** A `hermes gateway run` of this user's: the words to start it again with. */
|
|
26973
|
+
ours(info) {
|
|
26974
|
+
return info.uid === this.options.system.uid ? gatewayRunArgs(info.command) : void 0;
|
|
26921
26975
|
}
|
|
26922
|
-
/**
|
|
26923
|
-
|
|
26924
|
-
|
|
26976
|
+
/** SIGTERM, then SIGKILL once it has had a while, and only while it is still that gateway. */
|
|
26977
|
+
async stop(pid) {
|
|
26978
|
+
const { system, log: log2 } = this.options;
|
|
26979
|
+
system.signal(pid, "SIGTERM");
|
|
26980
|
+
if (await this.gone(pid, this.timing.stopMs)) return true;
|
|
26981
|
+
const still = await system.describe(pid);
|
|
26982
|
+
if (still === void 0) return true;
|
|
26983
|
+
if (this.ours(still) === void 0) return false;
|
|
26984
|
+
log2(`Hermes's gateway, pid ${pid}, is still running ${this.timing.stopMs / 1e3} s after SIGTERM: SIGKILL`);
|
|
26985
|
+
system.signal(pid, "SIGKILL");
|
|
26986
|
+
return this.gone(pid, this.timing.killMs);
|
|
26925
26987
|
}
|
|
26926
|
-
|
|
26927
|
-
|
|
26928
|
-
|
|
26929
|
-
|
|
26930
|
-
|
|
26931
|
-
|
|
26932
|
-
*/
|
|
26933
|
-
async seatMain(config3, known) {
|
|
26934
|
-
const { chips: chips2 } = this.options;
|
|
26935
|
-
const before = known.find((agent) => agent.lead);
|
|
26936
|
-
const made = known.some((agent) => agent.key === chips2.harnessId);
|
|
26937
|
-
if (!made && before !== void 0) {
|
|
26938
|
-
await this.goHome(before, known);
|
|
26939
|
-
return before;
|
|
26940
|
-
}
|
|
26941
|
-
const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
|
|
26942
|
-
await this.goHome(main2, known);
|
|
26943
|
-
if (before !== void 0 && before.agentId !== main2.agentId) {
|
|
26944
|
-
await chips2.agents.register({ key: before.key, name: before.name, state: "archived" }).catch((error62) => this.log(`${before.name} stays: ${String(error62)}`));
|
|
26988
|
+
async gone(pid, withinMs) {
|
|
26989
|
+
const { system } = this.options;
|
|
26990
|
+
const by = system.now() + withinMs;
|
|
26991
|
+
while (system.alive(pid)) {
|
|
26992
|
+
if (system.now() >= by) return false;
|
|
26993
|
+
await system.sleep(this.timing.pollMs);
|
|
26945
26994
|
}
|
|
26946
|
-
return
|
|
26995
|
+
return true;
|
|
26947
26996
|
}
|
|
26948
|
-
/**
|
|
26949
|
-
async
|
|
26950
|
-
const {
|
|
26951
|
-
|
|
26952
|
-
|
|
26953
|
-
|
|
26997
|
+
/** Hermes's own `hermes gateway restart|start`, then the gateway answering, from a new process. */
|
|
26998
|
+
async byService(gateway, verb, before) {
|
|
26999
|
+
const { system, env } = this.options;
|
|
27000
|
+
const hermes = system.which("hermes", env);
|
|
27001
|
+
if (hermes === void 0) return NO_HERMES;
|
|
27002
|
+
const scope = this.serviceFor(gateway)?.system === true ? ["--system"] : [];
|
|
27003
|
+
const ran = await system.run(hermes, ["gateway", verb, ...scope], gatewayEnv(env), this.timing.serviceMs);
|
|
27004
|
+
if (ran.code !== 0) return line(`hermes gateway ${verb} failed: ${whyFrom(ran.output, ran.code)}`);
|
|
27005
|
+
const by = system.now() + this.timing.serviceMs;
|
|
27006
|
+
for (; ; ) {
|
|
27007
|
+
const old = before !== void 0 && system.alive(before);
|
|
27008
|
+
if (!old && await system.healthy(gateway.url)) return void 0;
|
|
27009
|
+
if (system.now() >= by) {
|
|
27010
|
+
if (await system.healthy(gateway.url)) return void 0;
|
|
27011
|
+
return `Hermes's service ${verb === "start" ? "started" : "restarted"}, but nothing answers on port ${portOf(gateway.url)}.`;
|
|
27012
|
+
}
|
|
27013
|
+
await system.sleep(this.timing.pollMs);
|
|
27014
|
+
}
|
|
26954
27015
|
}
|
|
26955
27016
|
/**
|
|
26956
|
-
*
|
|
26957
|
-
*
|
|
26958
|
-
* room waits. It answers through the first profile, in a session of its own.
|
|
27017
|
+
* `hermes <args>` (`gateway run`), on its own, in `cwd` (else the home), then through Homebrew's
|
|
27018
|
+
* expat when it died of macOS's.
|
|
26959
27019
|
*/
|
|
26960
|
-
async
|
|
26961
|
-
const {
|
|
26962
|
-
|
|
26963
|
-
const
|
|
26964
|
-
|
|
26965
|
-
|
|
26966
|
-
|
|
26967
|
-
|
|
26968
|
-
|
|
26969
|
-
|
|
26970
|
-
|
|
26971
|
-
|
|
26972
|
-
|
|
27020
|
+
async start(gateway, args, cwd) {
|
|
27021
|
+
const { system, env, log: log2 } = this.options;
|
|
27022
|
+
const port = portOf(gateway.url);
|
|
27023
|
+
const hermes = system.which("hermes", env);
|
|
27024
|
+
if (hermes === void 0) return NO_HERMES;
|
|
27025
|
+
const folder2 = cwd ?? system.home;
|
|
27026
|
+
const plain = gatewayEnv(env);
|
|
27027
|
+
let outcome = await this.launch(gateway, hermes, args, plain, folder2, `hermes ${args.join(" ")}`);
|
|
27028
|
+
if (outcome.state === "ended" && outcome.early && outcome.output.includes(EXPAT_SYMBOL) && system.exists(EXPAT)) {
|
|
27029
|
+
const wrapper = wrapperOf(system.read(hermes) ?? "", plain);
|
|
27030
|
+
if (wrapper !== void 0) {
|
|
27031
|
+
const expat = dirname5(EXPAT);
|
|
27032
|
+
log2(
|
|
27033
|
+
`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}.`
|
|
27034
|
+
);
|
|
27035
|
+
const libraries = [expat, plain["DYLD_LIBRARY_PATH"]].filter((each) => each !== void 0 && each !== "").join(":");
|
|
27036
|
+
outcome = await this.launch(
|
|
27037
|
+
gateway,
|
|
27038
|
+
wrapper.entry,
|
|
27039
|
+
args,
|
|
27040
|
+
{ ...plain, ...wrapper.vars, DYLD_LIBRARY_PATH: libraries },
|
|
27041
|
+
folder2,
|
|
27042
|
+
`${wrapper.entry} ${args.join(" ")} with DYLD_LIBRARY_PATH=${expat}`
|
|
27043
|
+
);
|
|
27044
|
+
}
|
|
27045
|
+
}
|
|
27046
|
+
if (outcome.state === "up") {
|
|
27047
|
+
log2(`Hermes's gateway${outcome.pid === void 0 ? "" : `, pid ${outcome.pid},`} answers on port ${port}. It logs to ${this.options.logFile}.`);
|
|
27048
|
+
return void 0;
|
|
27049
|
+
}
|
|
27050
|
+
if (outcome.state === "silent") return `Hermes's gateway started, but nothing answers on port ${port}.`;
|
|
27051
|
+
return line(`Hermes's gateway stopped as it started: ${whyFrom(outcome.output, outcome.code)}`);
|
|
27052
|
+
}
|
|
27053
|
+
/** A program started, and watched until it answers, ends, or has had its time. */
|
|
27054
|
+
async launch(gateway, command, args, env, cwd, what) {
|
|
27055
|
+
const { system, logFile } = this.options;
|
|
27056
|
+
system.note(logFile, `--- ${new Date(system.now()).toISOString()} chips-hermes started ${what}`);
|
|
27057
|
+
const from = system.size(logFile);
|
|
27058
|
+
const startedAt = system.now();
|
|
27059
|
+
const child = system.launch({ command, args, env, cwd, log: logFile });
|
|
27060
|
+
let ended;
|
|
27061
|
+
void child.exited.then(({ code }) => {
|
|
27062
|
+
ended = { code, at: system.now() };
|
|
27063
|
+
});
|
|
27064
|
+
for (; ; ) {
|
|
27065
|
+
await Promise.resolve();
|
|
27066
|
+
if (ended !== void 0) {
|
|
27067
|
+
return { state: "ended", early: ended.at - startedAt <= this.timing.earlyMs, output: system.since(logFile, from), code: ended.code };
|
|
27068
|
+
}
|
|
27069
|
+
if (await system.healthy(gateway.url)) return { state: "up", pid: child.pid };
|
|
27070
|
+
if (system.now() - startedAt >= this.timing.upMs) return { state: "silent" };
|
|
27071
|
+
await system.sleep(this.timing.pollMs);
|
|
27072
|
+
}
|
|
27073
|
+
}
|
|
27074
|
+
};
|
|
27075
|
+
function restarter(deps) {
|
|
27076
|
+
let running;
|
|
27077
|
+
return (ask2) => {
|
|
27078
|
+
if (running !== void 0) {
|
|
27079
|
+
deps.log(`restart asked by ${ask2.askedBy} while one runs: it is the same restart`);
|
|
27080
|
+
return running;
|
|
27081
|
+
}
|
|
27082
|
+
running = (async () => {
|
|
27083
|
+
deps.log(`restart asked by ${ask2.askedBy}: restarting Hermes's gateway`);
|
|
27084
|
+
deps.hold(true);
|
|
27085
|
+
let trouble;
|
|
27086
|
+
try {
|
|
27087
|
+
trouble = await deps.restart();
|
|
27088
|
+
} catch (error62) {
|
|
27089
|
+
trouble = line(`Restart failed: ${error62 instanceof Error ? error62.message : String(error62)}`);
|
|
27090
|
+
}
|
|
27091
|
+
deps.log(trouble === void 0 ? "restarted, and Hermes answers" : `restart: ${trouble}`);
|
|
27092
|
+
try {
|
|
27093
|
+
await deps.say(trouble);
|
|
27094
|
+
} catch (error62) {
|
|
27095
|
+
deps.log(`could not tell Chips how the restart went: ${String(error62)}`);
|
|
27096
|
+
} finally {
|
|
27097
|
+
deps.hold(false);
|
|
27098
|
+
}
|
|
27099
|
+
})().finally(() => {
|
|
27100
|
+
running = void 0;
|
|
27101
|
+
});
|
|
27102
|
+
return running;
|
|
27103
|
+
};
|
|
27104
|
+
}
|
|
27105
|
+
function gatewayLog(configFolder, platform = process.platform) {
|
|
27106
|
+
return platform === "darwin" ? join8(homedir5(), "Library", "Logs", "chips-hermes-gateway.log") : join8(configFolder, "gateway.log");
|
|
27107
|
+
}
|
|
27108
|
+
function runToEnd(command, args, options) {
|
|
27109
|
+
return new Promise((done) => {
|
|
27110
|
+
const child = spawn2(command, args, { env: options.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
27111
|
+
let output2 = "";
|
|
27112
|
+
const keep = (data) => {
|
|
27113
|
+
output2 = (output2 + String(data)).slice(-8e3);
|
|
27114
|
+
};
|
|
27115
|
+
child.stdout.on("data", keep);
|
|
27116
|
+
child.stderr.on("data", keep);
|
|
27117
|
+
const limit = setTimeout(() => child.kill("SIGTERM"), options.timeoutMs);
|
|
27118
|
+
child.on("error", (error62) => {
|
|
27119
|
+
clearTimeout(limit);
|
|
27120
|
+
done({ code: null, output: error62.message });
|
|
27121
|
+
});
|
|
27122
|
+
child.on("close", (code) => {
|
|
27123
|
+
clearTimeout(limit);
|
|
27124
|
+
done({ code, output: output2 });
|
|
27125
|
+
});
|
|
27126
|
+
});
|
|
27127
|
+
}
|
|
27128
|
+
function thisMachine() {
|
|
27129
|
+
return {
|
|
27130
|
+
platform: process.platform,
|
|
27131
|
+
uid: process.getuid?.() ?? -1,
|
|
27132
|
+
home: homedir5(),
|
|
27133
|
+
exists: existsSync8,
|
|
27134
|
+
read(path) {
|
|
27135
|
+
try {
|
|
27136
|
+
return readFileSync8(path, "utf8");
|
|
27137
|
+
} catch {
|
|
27138
|
+
return void 0;
|
|
27139
|
+
}
|
|
27140
|
+
},
|
|
27141
|
+
which(command, env) {
|
|
27142
|
+
for (const folder2 of (env["PATH"] ?? "").split(delimiter2)) {
|
|
27143
|
+
if (folder2 === "") continue;
|
|
27144
|
+
const path = join8(folder2, command);
|
|
27145
|
+
try {
|
|
27146
|
+
accessSync2(path, constants2.X_OK);
|
|
27147
|
+
return path;
|
|
27148
|
+
} catch {
|
|
27149
|
+
}
|
|
27150
|
+
}
|
|
27151
|
+
return void 0;
|
|
27152
|
+
},
|
|
27153
|
+
async listener(port) {
|
|
27154
|
+
const { output: output2 } = await runToEnd("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-Fp"], { timeoutMs: 5e3 });
|
|
27155
|
+
const pid = Number(/^p(\d+)$/m.exec(output2)?.[1]);
|
|
27156
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
27157
|
+
},
|
|
27158
|
+
async describe(pid) {
|
|
27159
|
+
const { code, output: output2 } = await runToEnd("ps", ["-ww", "-o", "uid=", "-o", "command=", "-p", String(pid)], { timeoutMs: 5e3 });
|
|
27160
|
+
const found = /^\s*(\d+)\s+(.*)$/m.exec(output2);
|
|
27161
|
+
if (code !== 0 || found === null) return void 0;
|
|
27162
|
+
return { uid: Number(found[1]), command: found[2] ?? "", cwd: processFolder(pid) };
|
|
27163
|
+
},
|
|
27164
|
+
alive(pid) {
|
|
27165
|
+
try {
|
|
27166
|
+
process.kill(pid, 0);
|
|
27167
|
+
return true;
|
|
27168
|
+
} catch (error62) {
|
|
27169
|
+
return error62.code === "EPERM";
|
|
27170
|
+
}
|
|
27171
|
+
},
|
|
27172
|
+
signal(pid, signal) {
|
|
27173
|
+
try {
|
|
27174
|
+
process.kill(pid, signal);
|
|
27175
|
+
} catch {
|
|
27176
|
+
}
|
|
27177
|
+
},
|
|
27178
|
+
run: (command, args, env, timeoutMs) => runToEnd(command, args, { env, timeoutMs }),
|
|
27179
|
+
launch({ command, args, env, cwd, log: log2 }) {
|
|
27180
|
+
const out = openSync2(log2, "a");
|
|
27181
|
+
try {
|
|
27182
|
+
const child = spawn2(command, args, { cwd, env, detached: true, stdio: ["ignore", out, out] });
|
|
27183
|
+
const exited = new Promise((done) => {
|
|
27184
|
+
child.once("exit", (code, signal) => done({ code, signal }));
|
|
27185
|
+
child.once("error", (error62) => {
|
|
27186
|
+
appendFileSync2(log2, `${error62.message}
|
|
27187
|
+
`);
|
|
27188
|
+
done({ code: null, signal: null });
|
|
27189
|
+
});
|
|
27190
|
+
});
|
|
27191
|
+
child.unref();
|
|
27192
|
+
return { pid: child.pid, exited };
|
|
27193
|
+
} finally {
|
|
27194
|
+
closeSync2(out);
|
|
27195
|
+
}
|
|
27196
|
+
},
|
|
27197
|
+
note(log2, words4) {
|
|
27198
|
+
appendFileSync2(log2, `${words4}
|
|
27199
|
+
`);
|
|
27200
|
+
},
|
|
27201
|
+
size(log2) {
|
|
27202
|
+
try {
|
|
27203
|
+
return statSync3(log2).size;
|
|
27204
|
+
} catch {
|
|
27205
|
+
return 0;
|
|
27206
|
+
}
|
|
27207
|
+
},
|
|
27208
|
+
since(log2, from) {
|
|
27209
|
+
try {
|
|
27210
|
+
const to = statSync3(log2).size;
|
|
27211
|
+
const start = Math.max(from, to - 64e3);
|
|
27212
|
+
const buffer = Buffer.alloc(Math.max(0, to - start));
|
|
27213
|
+
const fd = openSync2(log2, "r");
|
|
27214
|
+
try {
|
|
27215
|
+
readSync(fd, buffer, 0, buffer.length, start);
|
|
27216
|
+
} finally {
|
|
27217
|
+
closeSync2(fd);
|
|
27218
|
+
}
|
|
27219
|
+
return buffer.toString("utf8");
|
|
27220
|
+
} catch {
|
|
27221
|
+
return "";
|
|
27222
|
+
}
|
|
27223
|
+
},
|
|
27224
|
+
healthy: (url2) => new HermesClient({ url: url2, key: "" }, (input2, init) => fetch(input2, { ...init, signal: AbortSignal.timeout(5e3) })).healthy(),
|
|
27225
|
+
sleep: (ms) => new Promise((done) => setTimeout(done, ms)),
|
|
27226
|
+
now: () => Date.now()
|
|
27227
|
+
};
|
|
27228
|
+
}
|
|
27229
|
+
|
|
27230
|
+
// src/profiles.ts
|
|
27231
|
+
var KEY_ENV = "API_SERVER_KEY";
|
|
27232
|
+
var RESERVED = /* @__PURE__ */ new Set(["hermes", "default", "test", "tmp", "root", "sudo"]);
|
|
27233
|
+
var CREATE_MS = 12e4;
|
|
27234
|
+
function freeProfile(root2, name2, taken) {
|
|
27235
|
+
const slug = name2.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 56).replace(/^-+|-+$/g, "") || "agent";
|
|
27236
|
+
for (let count = 1; ; count += 1) {
|
|
27237
|
+
if (count === 1 && RESERVED.has(slug)) continue;
|
|
27238
|
+
const profile = count === 1 ? slug : `${slug}-${count}`;
|
|
27239
|
+
if (!taken.includes(profile) && !existsSync9(join9(root2, "profiles", profile))) return profile;
|
|
27240
|
+
}
|
|
27241
|
+
}
|
|
27242
|
+
function readyProfile(home2, names, hook) {
|
|
27243
|
+
for (const file3 of ["MEMORY.md", "USER.md"]) rmSync4(join9(home2, "memories", file3), { force: true });
|
|
27244
|
+
if (envValue(home2, KEY_ENV) === void 0) appendEnv(home2, KEY_ENV, randomBytes2(32).toString("hex"));
|
|
27245
|
+
if (hook === void 0) return void 0;
|
|
27246
|
+
const path = join9(home2, "config.yaml");
|
|
27247
|
+
if (existsSync9(path)) {
|
|
27248
|
+
const theirs = hookUrl(hook.port, names.from).replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
27249
|
+
const entry = new RegExp(`^(\\s*(?:-\\s*)?url:\\s*["']?)${theirs}(["']?\\s*(?:#.*)?)$`, "m");
|
|
27250
|
+
const text = readFileSync9(path, "utf8");
|
|
27251
|
+
if (entry.test(text)) writeWhole(path, text.replace(entry, `$1${hookUrl(hook.port, names.profile)}$2`));
|
|
27252
|
+
}
|
|
27253
|
+
return addHook(home2, hook.port, names.profile, hook.secret);
|
|
27254
|
+
}
|
|
27255
|
+
function hermesProfiles(options) {
|
|
27256
|
+
return {
|
|
27257
|
+
async create(name2, from) {
|
|
27258
|
+
const args = ["profile", "create", name2, "--clone-from", from, "--no-alias"];
|
|
27259
|
+
const ran = await runHermes(options.system, options.env, args, CREATE_MS, options.log);
|
|
27260
|
+
if (ran.code !== 0) throw new Error(whyFrom(ran.output, ran.code).replace(/^Error:\s*/, ""));
|
|
27261
|
+
},
|
|
27262
|
+
async serve() {
|
|
27263
|
+
const result = await askGateway(options.root, "rescan-profiles");
|
|
27264
|
+
return typeof result?.["multiplex"] === "boolean" ? result["multiplex"] : void 0;
|
|
27265
|
+
}
|
|
27266
|
+
};
|
|
27267
|
+
}
|
|
27268
|
+
function controlSocketOf(root2) {
|
|
27269
|
+
const direct = join9(root2, "gateway.sock");
|
|
27270
|
+
if (existsSync9(direct)) return direct;
|
|
27271
|
+
try {
|
|
27272
|
+
const pointed = readFileSync9(join9(root2, "gateway.sock.path"), "utf8").trim();
|
|
27273
|
+
return pointed !== "" && existsSync9(pointed) ? pointed : void 0;
|
|
27274
|
+
} catch {
|
|
27275
|
+
return void 0;
|
|
27276
|
+
}
|
|
27277
|
+
}
|
|
27278
|
+
function askGateway(root2, verb, timeoutMs = 8e3) {
|
|
27279
|
+
const path = controlSocketOf(root2);
|
|
27280
|
+
if (path === void 0) return Promise.resolve(void 0);
|
|
27281
|
+
return new Promise((done) => {
|
|
27282
|
+
let heard = "";
|
|
27283
|
+
let over = false;
|
|
27284
|
+
const socket = createConnection(path);
|
|
27285
|
+
const finish = (line2) => {
|
|
27286
|
+
if (over) return;
|
|
27287
|
+
over = true;
|
|
27288
|
+
clearTimeout(timer);
|
|
27289
|
+
socket.destroy();
|
|
27290
|
+
done(line2 === void 0 ? void 0 : resultOf(line2));
|
|
27291
|
+
};
|
|
27292
|
+
const timer = setTimeout(() => finish(void 0), timeoutMs);
|
|
27293
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ verb, id: 1, protocol: 1 })}
|
|
27294
|
+
`));
|
|
27295
|
+
socket.on("data", (chunk) => {
|
|
27296
|
+
heard += String(chunk);
|
|
27297
|
+
const end2 = heard.indexOf("\n");
|
|
27298
|
+
if (end2 >= 0) finish(heard.slice(0, end2));
|
|
27299
|
+
});
|
|
27300
|
+
socket.on("end", () => finish(heard));
|
|
27301
|
+
socket.on("error", () => finish(void 0));
|
|
27302
|
+
});
|
|
27303
|
+
}
|
|
27304
|
+
function resultOf(line2) {
|
|
27305
|
+
try {
|
|
27306
|
+
const answer = JSON.parse(line2);
|
|
27307
|
+
return answer.ok === true && typeof answer.result === "object" && answer.result !== null ? answer.result : void 0;
|
|
27308
|
+
} catch {
|
|
27309
|
+
return void 0;
|
|
27310
|
+
}
|
|
27311
|
+
}
|
|
27312
|
+
|
|
27313
|
+
// src/bridge.ts
|
|
27314
|
+
var WANTS = [
|
|
27315
|
+
"agents",
|
|
27316
|
+
"new_spaces",
|
|
27317
|
+
"new_agents",
|
|
27318
|
+
"forks",
|
|
27319
|
+
"context_files",
|
|
27320
|
+
"models",
|
|
27321
|
+
"context_windows",
|
|
27322
|
+
"restarts"
|
|
27323
|
+
];
|
|
27324
|
+
function settingsFor(active) {
|
|
27325
|
+
const bridge2 = () => {
|
|
27326
|
+
const there = active();
|
|
27327
|
+
if (there === void 0) throw new Refuse("The bridge is starting. Try again in a moment.");
|
|
27328
|
+
return there;
|
|
27329
|
+
};
|
|
27330
|
+
return {
|
|
27331
|
+
context: (edit) => bridge2().onContextEdit(edit),
|
|
27332
|
+
models: (change) => bridge2().onModelChange(change),
|
|
27333
|
+
contextWindows: {
|
|
27334
|
+
compact: async (ask2) => bridge2().onWindowCompact(ask2),
|
|
27335
|
+
change: async (ask2) => bridge2().onWindowChange(ask2)
|
|
27336
|
+
}
|
|
27337
|
+
};
|
|
27338
|
+
}
|
|
27339
|
+
function filesHere(project = false) {
|
|
27340
|
+
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.`;
|
|
27341
|
+
}
|
|
27342
|
+
var FILES_HERE = filesHere();
|
|
27343
|
+
function instructionsFor(folder2, project = false) {
|
|
27344
|
+
const files = filesHere(project);
|
|
27345
|
+
return folder2 === void 0 ? files : `${files} Your own folder is ${folder2}: make and keep your files there, and run your commands from there.`;
|
|
27346
|
+
}
|
|
27347
|
+
function freeFolder(root2, name2, taken) {
|
|
27348
|
+
const slug = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
|
|
27349
|
+
for (let count = 1; ; count += 1) {
|
|
27350
|
+
const folder2 = join10(root2, count === 1 ? slug : `${slug}-${count}`);
|
|
27351
|
+
if (!taken.includes(folder2)) return folder2;
|
|
27352
|
+
}
|
|
27353
|
+
}
|
|
27354
|
+
var ids = createIdGenerator();
|
|
27355
|
+
var QUIET_MS = 1500;
|
|
27356
|
+
var LOST_STREAM_MS = 12e4;
|
|
27357
|
+
var KEPT_FEEDBACK = 500;
|
|
27358
|
+
var KINDS = {
|
|
27359
|
+
terminal: "shell",
|
|
27360
|
+
process: "shell",
|
|
27361
|
+
execute_code: "shell",
|
|
27362
|
+
web_search: "search",
|
|
27363
|
+
search_files: "search",
|
|
27364
|
+
session_search: "search",
|
|
27365
|
+
web_extract: "web",
|
|
27366
|
+
read_file: "file",
|
|
27367
|
+
write_file: "edit",
|
|
27368
|
+
patch: "edit",
|
|
27369
|
+
vision_analyze: "image",
|
|
27370
|
+
image_generate: "image",
|
|
27371
|
+
memory: "memory",
|
|
27372
|
+
delegate_task: "handoff"
|
|
27373
|
+
};
|
|
27374
|
+
function keyOf(agent) {
|
|
27375
|
+
return `hermes:${(agent.address.profile ?? "default").toLowerCase()}:${agent.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
|
27376
|
+
}
|
|
27377
|
+
var Bridge = class {
|
|
27378
|
+
constructor(options) {
|
|
27379
|
+
this.options = options;
|
|
27380
|
+
this.log = options.log ?? (() => void 0);
|
|
27381
|
+
const { chips: chips2, hermesRoot: hermesRoot2 } = options;
|
|
27382
|
+
if (hermesRoot2 === void 0) return;
|
|
27383
|
+
this.contexts = new Contexts(
|
|
27384
|
+
() => this.roster(),
|
|
27385
|
+
{
|
|
27386
|
+
agent: (agentId2, copy) => chips2.settings.context(agentId2, copy),
|
|
27387
|
+
harness: (copy) => chips2.settings.harnessContext(copy)
|
|
27388
|
+
},
|
|
27389
|
+
(line2) => this.log(`context: ${line2}`),
|
|
27390
|
+
options.env ?? process.env
|
|
27391
|
+
);
|
|
27392
|
+
if (options.fills === void 0) return;
|
|
27393
|
+
const kept = options.state.fills ??= {};
|
|
27394
|
+
this.fills = new Fills({
|
|
27395
|
+
whose: (sessionId) => this.whoseSession(sessionId),
|
|
27396
|
+
profileOf: (agentId2) => {
|
|
27397
|
+
const profile = this.profileFor(agentId2);
|
|
27398
|
+
return profile === void 0 ? void 0 : { profile: profile.name, home: profile.home };
|
|
27399
|
+
},
|
|
27400
|
+
secretFor: (profile) => envValue(profileHome(hermesRoot2, profile), SECRET_ENV) ?? kept.secret ?? options.fills?.secret,
|
|
27401
|
+
send: (agentId2, copy) => chips2.settings.contextWindow(agentId2, copy),
|
|
27402
|
+
kept,
|
|
27403
|
+
save: () => options.saveState(options.state),
|
|
27404
|
+
log: (line2) => this.log(`context window: ${line2}`),
|
|
27405
|
+
sendEveryMs: options.fillsEveryMs
|
|
27406
|
+
});
|
|
26973
27407
|
}
|
|
27408
|
+
options;
|
|
27409
|
+
agents = [];
|
|
27410
|
+
rooms = /* @__PURE__ */ new Map();
|
|
27411
|
+
/** Every Chips agent of the harness's, configured here or not. */
|
|
27412
|
+
ours = /* @__PURE__ */ new Set();
|
|
27413
|
+
/** The harness's agents' names, by Chips id, for the folders named after them. */
|
|
27414
|
+
names = /* @__PURE__ */ new Map();
|
|
27415
|
+
/** The agents made in Chips with a profile of their own, by Chips id (Chips ADR 0241). */
|
|
27416
|
+
own = /* @__PURE__ */ new Map();
|
|
27417
|
+
/** The feedback events answered with a turn, the newest `KEPT_FEEDBACK`. */
|
|
27418
|
+
lookedAt = /* @__PURE__ */ new Set();
|
|
26974
27419
|
/**
|
|
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.
|
|
27420
|
+
* The agents made, the spaces claimed and the copies made, one after another: a message for an
|
|
27421
|
+
* agent not settled yet waits for them (see `runnerOf`).
|
|
26984
27422
|
*/
|
|
26985
|
-
|
|
26986
|
-
|
|
26987
|
-
|
|
26988
|
-
|
|
26989
|
-
|
|
26990
|
-
|
|
26991
|
-
|
|
26992
|
-
|
|
27423
|
+
making = Promise.resolve();
|
|
27424
|
+
log;
|
|
27425
|
+
health;
|
|
27426
|
+
/** The harness's agents in Chips as last listed: whose context and window copies are sent. */
|
|
27427
|
+
listed = [];
|
|
27428
|
+
/** The agents' context files (Chips ADR 0134), when the bridge knows where Hermes keeps them. */
|
|
27429
|
+
contexts;
|
|
27430
|
+
contextTimer;
|
|
27431
|
+
/** The agents' context windows (Chips ADR 0146), when Hermes's webhook has somewhere to reach. */
|
|
27432
|
+
fills;
|
|
27433
|
+
/** The port Hermes's webhook reaches the bridge on, once it listens. */
|
|
27434
|
+
fillsPort;
|
|
27435
|
+
toldNoReadings = false;
|
|
27436
|
+
/** Whether the log has said once that a model copy wasn't sent. */
|
|
27437
|
+
toldModels = false;
|
|
27438
|
+
/** While Hermes's gateway restarts (Chips ADR 0094), the health watch says nothing: the restart says how it went. */
|
|
27439
|
+
held = false;
|
|
27440
|
+
/** What went wrong in the last restart, said in place of the watch's own words while Hermes doesn't answer. */
|
|
27441
|
+
why;
|
|
27442
|
+
/** Register the agents, start hearing what is said to them, and say how their gateways are. */
|
|
27443
|
+
async start() {
|
|
27444
|
+
const { chips: chips2 } = this.options;
|
|
27445
|
+
const known = await chips2.agents.list();
|
|
27446
|
+
for (const [index, config3] of this.options.agents.entries()) {
|
|
27447
|
+
const key = keyOf(config3);
|
|
27448
|
+
const registered = index === 0 ? await this.seatMain(config3, known) : await chips2.agents.register({ key, name: config3.name });
|
|
27449
|
+
const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
|
|
27450
|
+
this.agents.push({ config: config3, key, client, agentId: registered.agentId, up: true });
|
|
26993
27451
|
}
|
|
26994
|
-
const
|
|
26995
|
-
|
|
26996
|
-
|
|
26997
|
-
return;
|
|
27452
|
+
for (const [agentId2, profile] of Object.entries(this.options.state.made ?? {})) {
|
|
27453
|
+
const running = this.runningOn(agentId2, profile, known.find((agent) => agent.agentId === agentId2)?.name ?? profile);
|
|
27454
|
+
if (running !== void 0) this.own.set(agentId2, running);
|
|
26998
27455
|
}
|
|
26999
|
-
|
|
27000
|
-
|
|
27001
|
-
|
|
27002
|
-
|
|
27003
|
-
|
|
27004
|
-
|
|
27005
|
-
|
|
27006
|
-
|
|
27007
|
-
|
|
27008
|
-
|
|
27009
|
-
|
|
27010
|
-
|
|
27011
|
-
|
|
27012
|
-
|
|
27013
|
-
|
|
27014
|
-
|
|
27015
|
-
|
|
27016
|
-
this.
|
|
27017
|
-
|
|
27456
|
+
this.ours = /* @__PURE__ */ new Set([...known.map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
|
|
27457
|
+
chips2.on("message.new", (message) => void this.hear(message));
|
|
27458
|
+
chips2.on("agent.asked", (asked) => {
|
|
27459
|
+
this.making = this.making.then(() => this.make(asked)).catch((error62) => this.log(`a new agent went wrong: ${messageOf3(error62)}`));
|
|
27460
|
+
});
|
|
27461
|
+
chips2.on("agent.fork", (fork) => {
|
|
27462
|
+
this.making = this.making.then(() => this.copy(fork)).catch((error62) => this.log(`a copy went wrong: ${messageOf3(error62)}`));
|
|
27463
|
+
});
|
|
27464
|
+
chips2.onSpaceAsked((asked) => {
|
|
27465
|
+
this.making = this.making.then(() => this.claim(asked)).catch((error62) => this.log(`a space from the + went wrong: ${messageOf3(error62)}`));
|
|
27466
|
+
});
|
|
27467
|
+
chips2.on("feedback", (feedback2) => {
|
|
27468
|
+
this.notRight(feedback2).catch((error62) => this.log(`a note on a reply went wrong: ${messageOf3(error62)}`));
|
|
27469
|
+
});
|
|
27470
|
+
chips2.on("agent.attached", () => void this.agentsMoved());
|
|
27471
|
+
chips2.on("agent.released", () => void this.agentsMoved());
|
|
27472
|
+
await this.sayHowItIs();
|
|
27473
|
+
this.health = setInterval(() => void this.sayHowItIs(), this.options.healthEveryMs ?? 3e4);
|
|
27474
|
+
this.health.unref();
|
|
27475
|
+
await this.startSettings();
|
|
27018
27476
|
}
|
|
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;
|
|
27477
|
+
/** Hold the health watch's words while Hermes's gateway restarts, and let them go after. */
|
|
27478
|
+
hold(held) {
|
|
27479
|
+
this.held = held;
|
|
27026
27480
|
}
|
|
27027
27481
|
/**
|
|
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).
|
|
27482
|
+
* How a restart went (Chips ADR 0094): fine as the gateways now are, or its words, which stand
|
|
27483
|
+
* while Hermes doesn't answer. The health watch speaks for itself again after.
|
|
27033
27484
|
*/
|
|
27034
|
-
|
|
27035
|
-
|
|
27036
|
-
|
|
27037
|
-
if (
|
|
27038
|
-
|
|
27039
|
-
|
|
27040
|
-
|
|
27041
|
-
if (
|
|
27042
|
-
|
|
27043
|
-
|
|
27044
|
-
}
|
|
27045
|
-
return folder2;
|
|
27485
|
+
async restarted(trouble) {
|
|
27486
|
+
this.held = false;
|
|
27487
|
+
this.why = trouble;
|
|
27488
|
+
if (trouble === void 0) return this.sayHowItIs();
|
|
27489
|
+
await this.options.chips.reportTrouble(trouble).catch(() => void 0);
|
|
27490
|
+
}
|
|
27491
|
+
stop() {
|
|
27492
|
+
if (this.health !== void 0) clearInterval(this.health);
|
|
27493
|
+
if (this.contextTimer !== void 0) clearInterval(this.contextTimer);
|
|
27494
|
+
void this.fills?.close();
|
|
27046
27495
|
}
|
|
27496
|
+
// ------------------------------------------------------------------ an agent's page
|
|
27047
27497
|
/**
|
|
27048
|
-
* The
|
|
27049
|
-
*
|
|
27050
|
-
*
|
|
27498
|
+
* The agents' models, context and context windows, sent at start and kept current: the models
|
|
27499
|
+
* again when an agent comes or goes, the files read again every so often and after each turn, a
|
|
27500
|
+
* window copy on each reading Hermes sends.
|
|
27051
27501
|
*/
|
|
27052
|
-
async
|
|
27053
|
-
|
|
27054
|
-
|
|
27055
|
-
if (
|
|
27502
|
+
async startSettings() {
|
|
27503
|
+
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
27504
|
+
await this.sendModels();
|
|
27505
|
+
if (this.contexts === void 0) return;
|
|
27506
|
+
await this.contexts.refresh();
|
|
27507
|
+
this.contextTimer = setInterval(() => void this.contexts?.refresh(), this.options.contextEveryMs ?? 15e3);
|
|
27508
|
+
this.contextTimer.unref();
|
|
27509
|
+
const fills2 = this.fills;
|
|
27510
|
+
const asked = this.options.fills;
|
|
27511
|
+
if (fills2 === void 0 || asked === void 0) return;
|
|
27512
|
+
const kept = this.options.state.fills ??= {};
|
|
27056
27513
|
try {
|
|
27057
|
-
|
|
27058
|
-
|
|
27059
|
-
|
|
27060
|
-
|
|
27514
|
+
this.fillsPort = await fills2.listen(asked.port);
|
|
27515
|
+
if (kept.port !== this.fillsPort) {
|
|
27516
|
+
kept.port = this.fillsPort;
|
|
27517
|
+
this.options.saveState(this.options.state);
|
|
27518
|
+
}
|
|
27061
27519
|
} catch (error62) {
|
|
27062
|
-
this.log(
|
|
27063
|
-
return void 0;
|
|
27520
|
+
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
27521
|
}
|
|
27522
|
+
await fills2.sendAll();
|
|
27065
27523
|
}
|
|
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 };
|
|
27524
|
+
/** The address Hermes's webhook POSTs a profile's readings to, once the bridge listens. */
|
|
27525
|
+
hookUrl(profile = "default") {
|
|
27526
|
+
return this.fillsPort === void 0 ? void 0 : hookUrl(this.fillsPort, profileName(profile));
|
|
27527
|
+
}
|
|
27528
|
+
/** The harness's agents listed again, the copies of any gone forgotten, and the context and models sent. */
|
|
27529
|
+
async agentsMoved() {
|
|
27530
|
+
const before = this.listed;
|
|
27531
|
+
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
27532
|
+
const now = new Set(this.active().map((agent) => agent.agentId));
|
|
27533
|
+
for (const gone of before) {
|
|
27534
|
+
if (now.has(gone.agentId)) continue;
|
|
27535
|
+
this.options.chips.settings.forget(gone.agentId);
|
|
27536
|
+
this.fills?.forget(gone.agentId);
|
|
27084
27537
|
}
|
|
27538
|
+
await this.contexts?.refresh();
|
|
27539
|
+
await this.sendModels();
|
|
27540
|
+
}
|
|
27541
|
+
/** The harness's agents that read anything now: the active ones listed. */
|
|
27542
|
+
active() {
|
|
27543
|
+
return this.listed.filter((agent) => agent.state === "active");
|
|
27544
|
+
}
|
|
27545
|
+
/** A profile as the context needs it: its name and its home under Hermes's root. */
|
|
27546
|
+
profileOfRunning(agent) {
|
|
27547
|
+
const root2 = this.options.hermesRoot;
|
|
27548
|
+
if (root2 === void 0) return void 0;
|
|
27549
|
+
return { name: profileName(agent.config.address.profile), home: profileHome(root2, agent.config.address.profile) };
|
|
27550
|
+
}
|
|
27551
|
+
/** The profile an agent runs on: its own, a copy's original's, else the first (as `profileOf` picks). */
|
|
27552
|
+
profileFor(agentId2) {
|
|
27553
|
+
const running = this.runningFor(agentId2);
|
|
27554
|
+
return running === void 0 ? void 0 : this.profileOfRunning(running);
|
|
27085
27555
|
}
|
|
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);
|
|
27556
|
+
/** What answers for an agent: its own profile, a copy's original's, else the first. */
|
|
27557
|
+
runningFor(agentId2) {
|
|
27558
|
+
const own2 = this.agents.find((agent) => agent.agentId === agentId2) ?? this.own.get(agentId2);
|
|
27559
|
+
const copied = this.options.state.profiles?.[agentId2];
|
|
27560
|
+
return own2 ?? (copied === void 0 ? void 0 : this.byKey(copied)) ?? this.agents[0];
|
|
27096
27561
|
}
|
|
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
|
-
}
|
|
27562
|
+
/** A profile the bridge speaks to, by its key: a configured agent's, or a made agent's Chips id. */
|
|
27563
|
+
byKey(key) {
|
|
27564
|
+
return this.agents.find((agent) => agent.key === key) ?? this.own.get(key);
|
|
27110
27565
|
}
|
|
27111
27566
|
/**
|
|
27112
|
-
*
|
|
27113
|
-
*
|
|
27114
|
-
*
|
|
27115
|
-
* reaction is the person's own. Once per event, however often it is heard.
|
|
27567
|
+
* Who reads what: each active agent with its profile, and the first profile, whose SOUL.md each
|
|
27568
|
+
* new agent starts from (and whose every file each new agent reads, where none gets a profile of
|
|
27569
|
+
* its own).
|
|
27116
27570
|
*/
|
|
27117
|
-
|
|
27118
|
-
const
|
|
27119
|
-
const
|
|
27120
|
-
const
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
|
|
27124
|
-
if (this.lookedAt.size <= KEPT_FEEDBACK) break;
|
|
27125
|
-
this.lookedAt.delete(old);
|
|
27571
|
+
roster() {
|
|
27572
|
+
const readers = [];
|
|
27573
|
+
const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
|
|
27574
|
+
for (const agentId2 of ids2) {
|
|
27575
|
+
if (this.listed.some((agent) => agent.agentId === agentId2 && agent.state !== "active")) continue;
|
|
27576
|
+
const profile = this.profileFor(agentId2);
|
|
27577
|
+
if (profile !== void 0) readers.push({ agentId: agentId2, profile });
|
|
27126
27578
|
}
|
|
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] }
|
|
27579
|
+
const first = this.agents[0];
|
|
27580
|
+
return {
|
|
27581
|
+
readers,
|
|
27582
|
+
first: first === void 0 ? void 0 : this.profileOfRunning(first),
|
|
27583
|
+
ownProfiles: this.options.profiles !== void 0
|
|
27137
27584
|
};
|
|
27138
|
-
await this.hear(message, { goal: lookingAgain(note), sayWho: false });
|
|
27139
27585
|
}
|
|
27586
|
+
/** The agent a Hermes session the bridge keeps is, with its profile: nothing for any other. */
|
|
27587
|
+
whoseSession(sessionId) {
|
|
27588
|
+
const entry = Object.entries(this.options.state.sessions).find(([, session]) => session === sessionId);
|
|
27589
|
+
if (entry === void 0) return void 0;
|
|
27590
|
+
const who = entry[0].slice(0, entry[0].lastIndexOf("|"));
|
|
27591
|
+
const agentId2 = this.agents.find((agent) => agent.key === who)?.agentId ?? who;
|
|
27592
|
+
const profile = this.profileFor(agentId2);
|
|
27593
|
+
return profile === void 0 ? void 0 : { agentId: agentId2, profile: profile.name, home: profile.home };
|
|
27594
|
+
}
|
|
27595
|
+
/** A person's edit of a context file from Chips (Chips ADR 0134), saved and answered. */
|
|
27596
|
+
async onContextEdit(frame) {
|
|
27597
|
+
if (this.contexts === void 0) throw new Refuse("This bridge reads no context files.");
|
|
27598
|
+
if (frame.agent_id !== void 0 && !this.listed.some((agent) => agent.agentId === frame.agent_id)) {
|
|
27599
|
+
this.listed = await this.options.chips.agents.list().catch(() => this.listed);
|
|
27600
|
+
}
|
|
27601
|
+
return this.contexts.edit(frame);
|
|
27602
|
+
}
|
|
27603
|
+
// ------------------------------------------------------------------ an agent's model
|
|
27140
27604
|
/**
|
|
27141
|
-
*
|
|
27142
|
-
*
|
|
27605
|
+
* The model a person picked for an agent, as its copy names it: a choice's id, or the model its
|
|
27606
|
+
* config says each of its profile's runs asks for; nothing for Hermes's default.
|
|
27143
27607
|
*/
|
|
27144
|
-
|
|
27145
|
-
|
|
27146
|
-
return
|
|
27608
|
+
ownModel(agentId2, running) {
|
|
27609
|
+
const picked = this.options.state.models?.[agentId2];
|
|
27610
|
+
return picked === null ? void 0 : picked ?? running.config.model;
|
|
27611
|
+
}
|
|
27612
|
+
/** What a turn's runs ask Hermes for: the agent's own model, else nothing, and the profile's runs. */
|
|
27613
|
+
runModel(turn) {
|
|
27614
|
+
const picked = this.options.state.models?.[turn.who];
|
|
27615
|
+
if (picked === null) return {};
|
|
27616
|
+
if (picked !== void 0) return runModelOf(picked);
|
|
27617
|
+
return turn.agent.config.model === void 0 ? {} : { model: turn.agent.config.model };
|
|
27147
27618
|
}
|
|
27148
27619
|
/**
|
|
27149
|
-
*
|
|
27150
|
-
*
|
|
27151
|
-
*
|
|
27620
|
+
* Each agent's model, sent to Chips (Chips ADR 0141): the models Hermes has for its profile, read
|
|
27621
|
+
* once a profile, with the one it runs on. One whose Hermes doesn't answer is sent none, and the
|
|
27622
|
+
* log says so once.
|
|
27152
27623
|
*/
|
|
27153
|
-
async
|
|
27154
|
-
const
|
|
27155
|
-
|
|
27156
|
-
const
|
|
27157
|
-
const
|
|
27158
|
-
|
|
27159
|
-
|
|
27160
|
-
|
|
27161
|
-
|
|
27162
|
-
|
|
27163
|
-
|
|
27624
|
+
async sendModels() {
|
|
27625
|
+
const { chips: chips2 } = this.options;
|
|
27626
|
+
const ids2 = /* @__PURE__ */ new Set([...this.active().map((agent) => agent.agentId), ...this.agents.map((agent) => agent.agentId)]);
|
|
27627
|
+
const read = /* @__PURE__ */ new Map();
|
|
27628
|
+
for (const agentId2 of ids2) {
|
|
27629
|
+
const running = this.runningFor(agentId2);
|
|
27630
|
+
if (running === void 0) continue;
|
|
27631
|
+
try {
|
|
27632
|
+
let options = read.get(running);
|
|
27633
|
+
if (options === void 0) {
|
|
27634
|
+
options = running.client.modelOptions();
|
|
27635
|
+
read.set(running, options);
|
|
27636
|
+
}
|
|
27637
|
+
const copy = copyOf(await options, this.ownModel(agentId2, running));
|
|
27638
|
+
if (copy !== void 0) await chips2.settings.model(agentId2, copy);
|
|
27639
|
+
} catch (error62) {
|
|
27640
|
+
if (!this.toldModels) this.log(`models: none sent for ${running.config.name}: ${messageOf3(error62)}`);
|
|
27641
|
+
this.toldModels = true;
|
|
27164
27642
|
}
|
|
27165
27643
|
}
|
|
27166
|
-
return taker === this.options.chips.harnessId || this.ours.has(taker) ? this.agents[0] : void 0;
|
|
27167
27644
|
}
|
|
27168
27645
|
/**
|
|
27169
|
-
*
|
|
27170
|
-
*
|
|
27646
|
+
* A person picked an agent's model (Chips ADR 0141): one Hermes has for its profile, kept as the
|
|
27647
|
+
* model each of its runs asks for from now on, or Hermes's default (`null`), the profile's own.
|
|
27648
|
+
* The model it runs on already changes nothing. Hermes says no levels of effort, so none is taken.
|
|
27171
27649
|
*/
|
|
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;
|
|
27650
|
+
async onModelChange(frame) {
|
|
27651
|
+
const { state: state2 } = this.options;
|
|
27652
|
+
const agentId2 = frame.agent_id;
|
|
27653
|
+
const ours2 = this.ours.has(agentId2) || this.agents.some((agent) => agent.agentId === agentId2) || this.own.has(agentId2);
|
|
27654
|
+
const running = ours2 ? this.runningFor(agentId2) : void 0;
|
|
27655
|
+
if (running === void 0) throw new Refuse(`${agentId2} isn't one of this harness's agents.`);
|
|
27656
|
+
let options;
|
|
27225
27657
|
try {
|
|
27226
|
-
|
|
27658
|
+
options = await running.client.modelOptions();
|
|
27227
27659
|
} catch (error62) {
|
|
27228
|
-
|
|
27229
|
-
return;
|
|
27660
|
+
throw new Refuse(`Hermes didn't say which models it has: ${messageOf3(error62)}`);
|
|
27230
27661
|
}
|
|
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);
|
|
27662
|
+
const now = copyOf(options, this.ownModel(agentId2, running));
|
|
27663
|
+
if (now === void 0) throw new Refuse("Hermes has no model for this agent's profile.");
|
|
27664
|
+
if (frame.model === now.model) return now;
|
|
27665
|
+
if (frame.model !== null && !now.choices.some((choice) => choice.id === frame.model)) {
|
|
27666
|
+
throw new Refuse(`Hermes has no ${frame.model} for this agent's profile.`, now);
|
|
27249
27667
|
}
|
|
27668
|
+
state2.models = { ...state2.models, [agentId2]: frame.model };
|
|
27669
|
+
this.options.saveState(state2);
|
|
27670
|
+
const name2 = await this.nameOf(agentId2, running.config.name);
|
|
27671
|
+
this.log(`${name2} runs on ${frame.model ?? `Hermes's default (${now.default?.model ?? "its profile's"})`}, from Chips`);
|
|
27672
|
+
return copyOf(options, this.ownModel(agentId2, running)) ?? now;
|
|
27250
27673
|
}
|
|
27251
|
-
/**
|
|
27252
|
-
|
|
27253
|
-
|
|
27254
|
-
|
|
27255
|
-
|
|
27256
|
-
|
|
27257
|
-
|
|
27258
|
-
|
|
27674
|
+
/** A person's Compact now (Chips ADR 0146): Hermes can't be asked to compact from outside a run. */
|
|
27675
|
+
onWindowCompact(frame) {
|
|
27676
|
+
throw new Refuse(WHY_NOT, this.fills?.copyOf(frame.agent_id));
|
|
27677
|
+
}
|
|
27678
|
+
/** A person's limit (Chips ADR 0146): Hermes sets it in its profile's config, for all its agents. */
|
|
27679
|
+
onWindowChange(frame) {
|
|
27680
|
+
const profile = this.profileFor(frame.agent_id);
|
|
27681
|
+
throw new Refuse(whyReadOnly(profile?.name ?? "default"), this.fills?.copyOf(frame.agent_id));
|
|
27682
|
+
}
|
|
27683
|
+
/** The Main agent, the first registered, once started: who says the harness's first words. */
|
|
27684
|
+
get mainAgentId() {
|
|
27685
|
+
return this.agents[0]?.agentId;
|
|
27686
|
+
}
|
|
27687
|
+
/**
|
|
27688
|
+
* The Main agent: the one Chips made for the harness when it joined, keyed by the harness's own
|
|
27689
|
+
* id, registered as Main (Chips ADR 0183), so it is called what the config says and has no
|
|
27690
|
+
* second beside it. It goes home, and a Main of ours from before that, out of its room now, is
|
|
27691
|
+
* archived, which gives its name up. With no such agent (a harness from before them), the Main
|
|
27692
|
+
* Chips has is kept: a harness has one at most.
|
|
27693
|
+
*/
|
|
27694
|
+
async seatMain(config3, known) {
|
|
27695
|
+
const { chips: chips2 } = this.options;
|
|
27696
|
+
const before = known.find((agent) => agent.lead);
|
|
27697
|
+
const made = known.some((agent) => agent.key === chips2.harnessId);
|
|
27698
|
+
if (!made && before !== void 0) {
|
|
27699
|
+
await this.goHome(before, known);
|
|
27700
|
+
return before;
|
|
27259
27701
|
}
|
|
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);
|
|
27702
|
+
const main2 = await chips2.agents.register({ key: chips2.harnessId, name: config3.name, lead: true });
|
|
27703
|
+
await this.goHome(main2, known);
|
|
27704
|
+
if (before !== void 0 && before.agentId !== main2.agentId) {
|
|
27705
|
+
await chips2.agents.register({ key: before.key, name: before.name, state: "archived" }).catch((error62) => this.log(`${before.name} stays: ${String(error62)}`));
|
|
27267
27706
|
}
|
|
27707
|
+
return main2;
|
|
27268
27708
|
}
|
|
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;
|
|
27709
|
+
/** The Main agent into the harness's own room, in the place of whichever of ours is there. */
|
|
27710
|
+
async goHome(main2, known) {
|
|
27711
|
+
const { chips: chips2, home: home2 } = this.options;
|
|
27712
|
+
if (home2 === void 0 || main2.spaces.includes(home2)) return;
|
|
27713
|
+
const there = known.find((agent) => agent.agentId !== main2.agentId && agent.spaces.includes(home2));
|
|
27714
|
+
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
27715
|
}
|
|
27283
|
-
/**
|
|
27284
|
-
|
|
27285
|
-
|
|
27286
|
-
|
|
27287
|
-
|
|
27288
|
-
|
|
27289
|
-
this.options
|
|
27290
|
-
|
|
27291
|
-
|
|
27716
|
+
/**
|
|
27717
|
+
* A person asked for a new agent in a room (Chips ADR 0108): one of the harness's, keyed by the
|
|
27718
|
+
* ask so an ask heard twice makes one, registered for the room, which Chips puts it in while the
|
|
27719
|
+
* room waits. It answers through a Hermes profile of its own (`ownProfile`).
|
|
27720
|
+
*/
|
|
27721
|
+
async make(asked) {
|
|
27722
|
+
const { chips: chips2 } = this.options;
|
|
27723
|
+
if (asked.harness_id !== chips2.harnessId || asked.space_id === void 0) return;
|
|
27724
|
+
const agent = await chips2.agents.register({
|
|
27725
|
+
key: `hermes:asked:${asked.id}`,
|
|
27726
|
+
name: asked.name,
|
|
27727
|
+
madeFor: asked.id,
|
|
27728
|
+
spaceId: asked.space_id
|
|
27729
|
+
});
|
|
27730
|
+
this.names.set(agent.agentId, agent.name);
|
|
27731
|
+
this.log(`${agent.name} made${agent.attached === false ? `, not in the room: ${agent.why ?? "too late"}` : ""}`);
|
|
27732
|
+
await this.ownProfile(agent.agentId, agent.name);
|
|
27733
|
+
this.ours.add(agent.agentId);
|
|
27734
|
+
void this.agentsMoved();
|
|
27292
27735
|
}
|
|
27293
27736
|
/**
|
|
27294
|
-
* A
|
|
27295
|
-
*
|
|
27296
|
-
*
|
|
27737
|
+
* A Hermes profile of its own for an agent made in Chips (Chips ADR 0241), so it remembers only
|
|
27738
|
+
* its own conversations: named after the agent, cloned from the Main agent's profile so it starts
|
|
27739
|
+
* with its model, keys, tools, skills and SOUL.md, its memory empty (`profiles.ts`). The gateway
|
|
27740
|
+
* is asked to serve it now; one that serves only the profile it started with, as Hermes's does
|
|
27741
|
+
* when it started with one, is restarted once. Where Hermes doesn't make one, the agent runs on
|
|
27742
|
+
* the first profile, in a session of its own, and the log says why.
|
|
27297
27743
|
*/
|
|
27298
|
-
async
|
|
27299
|
-
|
|
27300
|
-
|
|
27301
|
-
|
|
27302
|
-
|
|
27744
|
+
async ownProfile(agentId2, name2) {
|
|
27745
|
+
const { profiles, hermesRoot: root2, state: state2 } = this.options;
|
|
27746
|
+
const main2 = this.agents[0];
|
|
27747
|
+
if (profiles === void 0 || root2 === void 0 || main2 === void 0 || this.own.has(agentId2)) return;
|
|
27748
|
+
const from = profileName(main2.config.address.profile);
|
|
27749
|
+
const profile = freeProfile(root2, name2, Object.values(state2.made ?? {}));
|
|
27750
|
+
const home2 = profileHome(root2, profile);
|
|
27751
|
+
const secret = state2.fills?.secret ?? this.options.fills?.secret;
|
|
27752
|
+
try {
|
|
27753
|
+
await profiles.create(profile, from);
|
|
27754
|
+
const port = this.fillsPort;
|
|
27755
|
+
const hook = readyProfile(home2, { from, profile }, port === void 0 || secret === void 0 ? void 0 : { port, secret });
|
|
27756
|
+
if (hook?.state === "by_hand") this.log(`context window: ${hook.path} was left as it is: ${hook.why}. Add these lines to it:
|
|
27757
|
+
${hook.lines}`);
|
|
27758
|
+
} catch (error62) {
|
|
27759
|
+
this.log(`${name2} runs on the ${from} profile, in a session of its own: Hermes didn't make it one (${messageOf3(error62)})`);
|
|
27303
27760
|
return;
|
|
27304
27761
|
}
|
|
27305
|
-
|
|
27306
|
-
|
|
27307
|
-
|
|
27308
|
-
|
|
27309
|
-
|
|
27310
|
-
|
|
27311
|
-
|
|
27762
|
+
state2.made = { ...state2.made, [agentId2]: profile };
|
|
27763
|
+
this.options.saveState(state2);
|
|
27764
|
+
const running = this.runningOn(agentId2, profile, name2);
|
|
27765
|
+
if (running !== void 0) this.own.set(agentId2, running);
|
|
27766
|
+
this.log(`${name2} has a Hermes profile of its own, ${profile}, cloned from ${from}`);
|
|
27767
|
+
const served = await profiles.serve().catch(() => void 0);
|
|
27768
|
+
if (served === false) {
|
|
27769
|
+
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`);
|
|
27770
|
+
await this.options.restartGateway?.();
|
|
27771
|
+
} else if (served === void 0) {
|
|
27772
|
+
this.log(`Hermes's gateway didn't answer on its control socket: it serves ${profile} once it looks again, or starts again`);
|
|
27312
27773
|
}
|
|
27313
|
-
turn.next.push(text);
|
|
27314
27774
|
}
|
|
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
|
-
}
|
|
27775
|
+
/**
|
|
27776
|
+
* A profile made for an agent, as the bridge reaches it: under `/p/<profile>` of the gateway the
|
|
27777
|
+
* Main agent is reached at, with the key in the profile's own `.env`.
|
|
27778
|
+
*/
|
|
27779
|
+
runningOn(agentId2, profile, name2) {
|
|
27780
|
+
const main2 = this.agents[0];
|
|
27781
|
+
const root2 = this.options.hermesRoot;
|
|
27782
|
+
if (main2 === void 0 || root2 === void 0) return void 0;
|
|
27783
|
+
const home2 = profileHome(root2, profile);
|
|
27784
|
+
const config3 = {
|
|
27785
|
+
name: name2,
|
|
27786
|
+
address: { url: main2.config.address.url, profile, key: envValue(home2, KEY_ENV) ?? "" },
|
|
27787
|
+
keyEnv: KEY_ENV,
|
|
27788
|
+
keyFrom: join10(home2, ".env")
|
|
27789
|
+
};
|
|
27790
|
+
const client = this.options.clientFor?.(config3) ?? new HermesClient(config3.address);
|
|
27791
|
+
return { config: config3, key: agentId2, client, agentId: agentId2, up: true };
|
|
27343
27792
|
}
|
|
27344
|
-
|
|
27345
|
-
|
|
27346
|
-
|
|
27347
|
-
|
|
27348
|
-
|
|
27349
|
-
|
|
27793
|
+
/**
|
|
27794
|
+
* A space a person asked the harness for from the + (Chips ADR 0241), as the ACP bridge takes
|
|
27795
|
+
* one: claimed under a key made from the space, so a space handed over twice is claimed once,
|
|
27796
|
+
* and named from its first words. Chips makes the agent of that key and puts it in the space; it
|
|
27797
|
+
* gets a Hermes profile of its own (`ownProfile`). It answers the first words, which are the
|
|
27798
|
+
* ask's and aren't heard again.
|
|
27799
|
+
*/
|
|
27800
|
+
async claim(asked) {
|
|
27801
|
+
const { chips: chips2 } = this.options;
|
|
27802
|
+
const { spaceId, firstMessage } = asked;
|
|
27803
|
+
const key = `hermes:space:${spaceId}`;
|
|
27804
|
+
await chips2.claimSpace(spaceId, key, titleFrom(firstMessage.text));
|
|
27805
|
+
const agent = (await chips2.agents.list()).find((each) => each.key === key);
|
|
27806
|
+
if (agent === void 0) {
|
|
27807
|
+
this.log(`claimed ${spaceId}, and Chips put no agent there`);
|
|
27808
|
+
return;
|
|
27809
|
+
}
|
|
27810
|
+
this.names.set(agent.agentId, agent.name);
|
|
27811
|
+
this.log(`${agent.name} claimed ${spaceId}`);
|
|
27812
|
+
await this.ownProfile(agent.agentId, agent.name);
|
|
27813
|
+
this.ours.add(agent.agentId);
|
|
27814
|
+
void this.agentsMoved();
|
|
27815
|
+
const message = {
|
|
27816
|
+
type: "message.new",
|
|
27817
|
+
space_id: spaceId,
|
|
27818
|
+
message_id: firstMessage.messageId,
|
|
27819
|
+
text: firstMessage.text,
|
|
27820
|
+
from: { participant_id: asked.askedBy, kind: "person", display_name: await this.whoSaid(asked), role: "member" },
|
|
27821
|
+
for: [agent.agentId]
|
|
27822
|
+
};
|
|
27823
|
+
void this.hear(message);
|
|
27350
27824
|
}
|
|
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);
|
|
27825
|
+
/** Who said a space's first words, from its lines. */
|
|
27826
|
+
async whoSaid(asked) {
|
|
27827
|
+
const context = await this.options.chips.roomContext(asked.spaceId, { limit: 20 }).catch(() => void 0);
|
|
27828
|
+
return context?.lines.find((line2) => line2.message_id === asked.firstMessage.messageId)?.from.name ?? "Someone";
|
|
27367
27829
|
}
|
|
27368
|
-
|
|
27369
|
-
|
|
27370
|
-
|
|
27371
|
-
|
|
27372
|
-
|
|
27373
|
-
|
|
27374
|
-
|
|
27375
|
-
|
|
27376
|
-
|
|
27377
|
-
|
|
27378
|
-
|
|
27379
|
-
|
|
27830
|
+
/**
|
|
27831
|
+
* A person asked for a copy of one of the harness's agents in another room (ADR 0087, Fork): the
|
|
27832
|
+
* agent's session in the room it is in, else the last it had, forked in Hermes, is the copy's
|
|
27833
|
+
* session in the room asked. The copy answers through its original's profile, whose Hermes has
|
|
27834
|
+
* that session (Hermes forks a session only within its profile, so a copy gets no profile of its
|
|
27835
|
+
* own: Chips ADR 0241), and works in its original's folder. It is registered with `forkedFrom` for the
|
|
27836
|
+
* room, which Chips puts it in while the room waits. The fork and the copy are keyed by the ask,
|
|
27837
|
+
* so an ask heard twice forks once (Hermes refuses the id again) and registers the same copy. An
|
|
27838
|
+
* agent that never spoke is copied with nothing to remember. A turn still going is not in the
|
|
27839
|
+
* copy: Hermes copies what the session holds. Nothing is made for an ask the room has stopped
|
|
27840
|
+
* waiting for (`FORK_WAIT_MS`), nor for an agent that isn't the harness's.
|
|
27841
|
+
*/
|
|
27842
|
+
async copy(fork) {
|
|
27843
|
+
const { chips: chips2, state: state2 } = this.options;
|
|
27844
|
+
const spaceId = fork.space_id;
|
|
27845
|
+
if (spaceId === void 0) return;
|
|
27846
|
+
const late = Date.now() - Date.parse(fork.ts);
|
|
27847
|
+
if (late > FORK_WAIT_MS) {
|
|
27848
|
+
this.log(`no copy of ${fork.agent_id}: asked ${Math.round(late / 1e3)} s ago, and the room waits a minute`);
|
|
27849
|
+
return;
|
|
27380
27850
|
}
|
|
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);
|
|
27851
|
+
const original = await this.profileOf(fork.agent_id);
|
|
27852
|
+
if (original === void 0) {
|
|
27853
|
+
this.log(`no copy of ${fork.agent_id}: it isn't one of this harness's agents`);
|
|
27854
|
+
return;
|
|
27394
27855
|
}
|
|
27856
|
+
const name2 = await this.nameOf(fork.agent_id, original.config.name);
|
|
27857
|
+
const key = `hermes:fork:${fork.id}`;
|
|
27858
|
+
const who = fork.agent_id === original.agentId ? original.key : fork.agent_id;
|
|
27859
|
+
const listed = await chips2.agents.list().catch(() => []);
|
|
27860
|
+
const room = listed.find((agent) => agent.agentId === fork.agent_id)?.room ?? void 0;
|
|
27861
|
+
const from = (room === void 0 ? void 0 : state2.sessions[`${who}|${room}`]) ?? Object.entries(state2.sessions).findLast(([id2]) => id2.startsWith(`${who}|`))?.[1];
|
|
27862
|
+
if (from === void 0) this.log(`${name2} never spoke: its copy remembers nothing`);
|
|
27863
|
+
const session = from === void 0 ? void 0 : await original.client.forkSession(from, { id: `chips-fork-${fork.id}`, title: `Chips space ${spaceId} \xB7 ${key}` });
|
|
27864
|
+
const folder2 = this.folderOf(original, fork.agent_id, name2);
|
|
27865
|
+
const copy = await chips2.agents.register({ key, name: forkName(name2), forkedFrom: fork.agent_id, spaceId });
|
|
27866
|
+
if (session !== void 0) state2.sessions[`${copy.agentId}|${spaceId}`] ??= session;
|
|
27867
|
+
state2.profiles = { ...state2.profiles, [copy.agentId]: original.key };
|
|
27868
|
+
if (folder2 !== void 0) state2.folders = { ...state2.folders, [copy.agentId]: folder2 };
|
|
27869
|
+
this.options.saveState(state2);
|
|
27870
|
+
this.ours.add(copy.agentId);
|
|
27871
|
+
this.names.set(copy.agentId, copy.name);
|
|
27872
|
+
const away = copy.attached === false && !copy.spaces.includes(spaceId);
|
|
27873
|
+
this.log(`${copy.name}, a copy of ${name2}, made${away ? `, not in the room: ${copy.why ?? "too late"}` : ""}`);
|
|
27874
|
+
void this.agentsMoved();
|
|
27395
27875
|
}
|
|
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 });
|
|
27876
|
+
/** An agent's name, from Chips's list when it isn't known yet. */
|
|
27877
|
+
async nameOf(agentId2, otherwise) {
|
|
27878
|
+
if (!this.names.has(agentId2)) {
|
|
27879
|
+
const listed = await this.options.chips.agents.list().catch(() => []);
|
|
27880
|
+
for (const agent of listed) this.names.set(agent.agentId, agent.name);
|
|
27473
27881
|
}
|
|
27474
|
-
|
|
27475
|
-
|
|
27476
|
-
|
|
27477
|
-
|
|
27478
|
-
|
|
27882
|
+
return this.names.get(agentId2) ?? otherwise;
|
|
27883
|
+
}
|
|
27884
|
+
/**
|
|
27885
|
+
* The folder an agent works in and sends its files from: the one the config names for them all
|
|
27886
|
+
* (`folder`, as it was), else one of its own under `folders`, named after it the first time it
|
|
27887
|
+
* takes a turn and kept, so a rename keeps it. Every agent once shared one folder, and a team's
|
|
27888
|
+
* files overwrote a one-agent room's game (Chips TT-16); and the one folder had to be Hermes's
|
|
27889
|
+
* own working folder, which a plain Hermes doesn't set (the setup audit, 17 October 2026).
|
|
27890
|
+
*/
|
|
27891
|
+
folderOf(agent, agentId2, name2) {
|
|
27892
|
+
if (agent.config.folder !== void 0) return agent.config.folder;
|
|
27893
|
+
const { folders: root2, state: state2 } = this.options;
|
|
27894
|
+
if (root2 === void 0) return void 0;
|
|
27895
|
+
const kept = state2.folders?.[agentId2];
|
|
27896
|
+
const folder2 = kept ?? freeFolder(root2, name2, Object.values(state2.folders ?? {}));
|
|
27897
|
+
mkdirSync5(folder2, { recursive: true });
|
|
27898
|
+
if (kept === void 0) {
|
|
27899
|
+
state2.folders = { ...state2.folders, [agentId2]: folder2 };
|
|
27900
|
+
this.options.saveState(state2);
|
|
27901
|
+
}
|
|
27902
|
+
return folder2;
|
|
27479
27903
|
}
|
|
27480
27904
|
/**
|
|
27481
|
-
*
|
|
27482
|
-
*
|
|
27483
|
-
*
|
|
27905
|
+
* The room's project for a turn (Chips ADR 0221): the agent's copy, in its folder, brought up to
|
|
27906
|
+
* date, and what the turn is told of it. None for an agent a person pointed at a folder of their
|
|
27907
|
+
* own, nor where this machine has no git for it.
|
|
27484
27908
|
*/
|
|
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);
|
|
27909
|
+
async projectFor(turn, spaceId, agentId2, name2) {
|
|
27910
|
+
if (this.options.projects !== true || turn.agent.config.folder !== void 0) return void 0;
|
|
27911
|
+
const folder2 = turn.folder;
|
|
27912
|
+
if (folder2 === void 0) return void 0;
|
|
27913
|
+
try {
|
|
27914
|
+
const project = this.options.chips.project({ spaceId, agentId: agentId2, name: name2, folder: folder2, log: (line2) => this.log(line2) });
|
|
27915
|
+
const facts = await project.pull();
|
|
27916
|
+
turn.project = { copy: project, name: name2 };
|
|
27917
|
+
return facts;
|
|
27918
|
+
} catch (error62) {
|
|
27919
|
+
this.log(`${name2}'s copy of the room's project couldn't be readied: ${messageOf3(error62)}`);
|
|
27920
|
+
return void 0;
|
|
27507
27921
|
}
|
|
27508
27922
|
}
|
|
27509
27923
|
/**
|
|
27510
|
-
*
|
|
27511
|
-
* `
|
|
27512
|
-
*
|
|
27513
|
-
* counts go with it, and are left out where Hermes doesn't say them.
|
|
27924
|
+
* What the turn changed in its copy, sent to the room's project before its words are posted: the
|
|
27925
|
+
* commit it left `main` at, for the reply to carry (Chips ADR 0222), when it sent one; else, when
|
|
27926
|
+
* the change didn't go, the line the reply says so in (Chips AP-6).
|
|
27514
27927
|
*/
|
|
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);
|
|
27928
|
+
async sendProject(turn, words4) {
|
|
27929
|
+
if (turn.project === void 0) return {};
|
|
27930
|
+
const { copy, name: name2 } = turn.project;
|
|
27931
|
+
try {
|
|
27932
|
+
const pushed = await copy.push({ words: words4 });
|
|
27933
|
+
this.log(`${name2}'s project: ${pushedInWords(pushed)}`);
|
|
27934
|
+
if (pushed.state === "sent") return { change: pushed.commit };
|
|
27935
|
+
const note = heldNote(pushed, name2);
|
|
27936
|
+
return note === void 0 ? {} : { note };
|
|
27937
|
+
} catch (error62) {
|
|
27938
|
+
this.log(`${name2}'s project couldn't be sent: ${messageOf3(error62)}`);
|
|
27939
|
+
const note = heldNote({ state: "failed", why: messageOf3(error62) }, name2);
|
|
27940
|
+
return note === void 0 ? {} : { note };
|
|
27535
27941
|
}
|
|
27536
|
-
turn.open.clear();
|
|
27537
27942
|
}
|
|
27538
|
-
|
|
27539
|
-
|
|
27540
|
-
if (
|
|
27943
|
+
/** Trouble while a profile's gateway doesn't answer, and fine once all do (ADR 0083); nothing while a restart runs. */
|
|
27944
|
+
async sayHowItIs() {
|
|
27945
|
+
if (this.held) return;
|
|
27946
|
+
for (const agent of this.agents) agent.up = await agent.client.healthy();
|
|
27947
|
+
if (this.held) return;
|
|
27948
|
+
const down = this.agents.filter((agent) => !agent.up);
|
|
27949
|
+
if (down.length === 0) this.why = void 0;
|
|
27950
|
+
const chips2 = this.options.chips;
|
|
27951
|
+
const said2 = down.length === 0 ? chips2.reportOk() : chips2.reportTrouble(this.why ?? `Hermes isn't answering for ${down.map((agent) => agent.config.name).join(", ")}`);
|
|
27952
|
+
await said2.catch(() => void 0);
|
|
27541
27953
|
}
|
|
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;
|
|
27954
|
+
// ------------------------------------------------------------------ what is said to it
|
|
27955
|
+
/** A message for some of its agents: each takes a turn in its room, one after another there. */
|
|
27956
|
+
async hear(message, how = {}) {
|
|
27957
|
+
const spaceId = message.space_id;
|
|
27958
|
+
if (spaceId === void 0) return;
|
|
27959
|
+
const harnessId = this.options.chips.harnessId;
|
|
27960
|
+
for (const taker of message.for ?? [harnessId]) {
|
|
27961
|
+
const agent = await this.runnerOf(taker);
|
|
27962
|
+
if (agent === void 0) continue;
|
|
27963
|
+
const room = this.roomOf(agent, taker, spaceId);
|
|
27964
|
+
room.queue.push({ message, speakerId: taker === harnessId ? void 0 : taker, ...how });
|
|
27965
|
+
void this.pump(room);
|
|
27966
|
+
}
|
|
27593
27967
|
}
|
|
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;
|
|
27968
|
+
/**
|
|
27969
|
+
* "Not right?" on one of its agents' replies (Chips ADR 0200): a person's note on it, or a bare
|
|
27970
|
+
* 👎 from an older phone, is a turn of the agent that said it, in its session in that room and
|
|
27971
|
+
* in line with the room's other turns, to look again and answer in its place. Any other
|
|
27972
|
+
* reaction is the person's own. Once per event, however often it is heard.
|
|
27973
|
+
*/
|
|
27974
|
+
async notRight(feedback2) {
|
|
27975
|
+
const { space_id: spaceId, message_id: messageId } = feedback2;
|
|
27976
|
+
const note = feedback2.kind === "correction" ? (feedback2.text ?? "").trim() : void 0;
|
|
27977
|
+
const flagged = note === void 0 ? feedback2.kind === "reaction" && feedback2.reaction === "\u{1F44E}" : note !== "";
|
|
27978
|
+
if (spaceId === void 0 || !messageId || !flagged || this.lookedAt.has(feedback2.id)) return;
|
|
27979
|
+
this.lookedAt.add(feedback2.id);
|
|
27980
|
+
for (const old of this.lookedAt) {
|
|
27981
|
+
if (this.lookedAt.size <= KEPT_FEEDBACK) break;
|
|
27982
|
+
this.lookedAt.delete(old);
|
|
27983
|
+
}
|
|
27984
|
+
const context = await this.options.chips.roomContext(spaceId, { limit: 200 }).catch(() => void 0);
|
|
27985
|
+
const line2 = context?.lines.find((each) => each.message_id === messageId);
|
|
27986
|
+
const message = {
|
|
27987
|
+
type: "message.new",
|
|
27988
|
+
space_id: spaceId,
|
|
27989
|
+
// An id of its own, so nothing of the turn is taken for the reply's.
|
|
27990
|
+
message_id: messageIdOf(feedback2.id),
|
|
27991
|
+
text: lookAgain(feedback2.from.display_name, line2?.text, note),
|
|
27992
|
+
from: feedback2.from,
|
|
27993
|
+
...line2 === void 0 ? {} : { for: [line2.from.id] }
|
|
27994
|
+
};
|
|
27995
|
+
await this.hear(message, { goal: lookingAgain(note), sayWho: false });
|
|
27631
27996
|
}
|
|
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
|
-
|
|
27997
|
+
/**
|
|
27998
|
+
* Which profile answers for a Chips agent, as `profileOf` says. An agent not settled may be one
|
|
27999
|
+
* being made, or a copy, which is waited for, so that its first message is answered on its own
|
|
28000
|
+
* profile, or in the session it was forked.
|
|
28001
|
+
*/
|
|
28002
|
+
async runnerOf(taker) {
|
|
28003
|
+
const settled = taker === this.options.chips.harnessId || this.own.has(taker) || this.agents.some((agent) => agent.agentId === taker);
|
|
28004
|
+
if (!settled) await this.making;
|
|
28005
|
+
return this.profileOf(taker);
|
|
28006
|
+
}
|
|
28007
|
+
/**
|
|
28008
|
+
* Which profile answers for a Chips agent: its own, configured or made for it; a copy's
|
|
28009
|
+
* original's; or the first for the harness itself and for any other of the harness's — its
|
|
28010
|
+
* default agent, one made before profiles were — read afresh once when a name is new.
|
|
28011
|
+
*/
|
|
28012
|
+
async profileOf(taker) {
|
|
28013
|
+
const own2 = this.agents.find((agent) => agent.agentId === taker) ?? this.own.get(taker);
|
|
28014
|
+
if (own2 !== void 0) return own2;
|
|
28015
|
+
const copied = this.options.state.profiles?.[taker];
|
|
28016
|
+
const original = copied === void 0 ? void 0 : this.byKey(copied);
|
|
28017
|
+
if (original !== void 0) return original;
|
|
28018
|
+
if (taker !== this.options.chips.harnessId && !this.ours.has(taker)) {
|
|
28019
|
+
const listed = await this.options.chips.agents.list().catch(() => []);
|
|
28020
|
+
for (const agent of listed) {
|
|
28021
|
+
this.ours.add(agent.agentId);
|
|
28022
|
+
this.names.set(agent.agentId, agent.name);
|
|
28023
|
+
}
|
|
28024
|
+
}
|
|
28025
|
+
return taker === this.options.chips.harnessId || this.ours.has(taker) ? this.agents[0] : void 0;
|
|
27657
28026
|
}
|
|
27658
|
-
|
|
27659
|
-
|
|
27660
|
-
|
|
27661
|
-
|
|
27662
|
-
|
|
27663
|
-
|
|
27664
|
-
|
|
27665
|
-
|
|
27666
|
-
|
|
28027
|
+
/**
|
|
28028
|
+
* The session a turn is taken in: the profile's own in the room, for its agent or the harness
|
|
28029
|
+
* itself, and one of its own for any other of the harness's, so two in a room are two.
|
|
28030
|
+
*/
|
|
28031
|
+
roomOf(agent, taker, spaceId) {
|
|
28032
|
+
const own2 = taker === agent.agentId || taker === this.options.chips.harnessId;
|
|
28033
|
+
const id2 = `${own2 ? agent.key : taker}|${spaceId}`;
|
|
28034
|
+
let room = this.rooms.get(id2);
|
|
28035
|
+
if (room === void 0) {
|
|
28036
|
+
room = { id: id2, agent, spaceId, queue: [], busy: false };
|
|
28037
|
+
this.rooms.set(id2, room);
|
|
28038
|
+
}
|
|
28039
|
+
return room;
|
|
27667
28040
|
}
|
|
27668
|
-
|
|
27669
|
-
|
|
27670
|
-
const
|
|
27671
|
-
if (
|
|
27672
|
-
|
|
27673
|
-
|
|
28041
|
+
async pump(room) {
|
|
28042
|
+
if (room.busy) return;
|
|
28043
|
+
const asked = room.queue.shift();
|
|
28044
|
+
if (asked === void 0) return;
|
|
28045
|
+
room.busy = true;
|
|
28046
|
+
try {
|
|
28047
|
+
await this.turn(room, asked);
|
|
28048
|
+
} catch (error62) {
|
|
28049
|
+
this.log(`a turn of ${room.agent.config.name} went wrong: ${messageOf3(error62)}`);
|
|
28050
|
+
} finally {
|
|
28051
|
+
room.busy = false;
|
|
28052
|
+
void this.contexts?.refresh();
|
|
28053
|
+
void this.pump(room);
|
|
28054
|
+
}
|
|
27674
28055
|
}
|
|
27675
|
-
|
|
27676
|
-
}
|
|
27677
|
-
|
|
27678
|
-
|
|
27679
|
-
|
|
27680
|
-
|
|
27681
|
-
|
|
27682
|
-
|
|
27683
|
-
|
|
27684
|
-
|
|
27685
|
-
|
|
27686
|
-
|
|
27687
|
-
|
|
27688
|
-
|
|
27689
|
-
|
|
27690
|
-
|
|
27691
|
-
|
|
27692
|
-
|
|
27693
|
-
|
|
28056
|
+
async turn(room, { message, speakerId, goal, sayWho }) {
|
|
28057
|
+
const { agent, spaceId } = room;
|
|
28058
|
+
const speaker = speakerId === void 0 ? this.options.chips : this.options.chips.as(speakerId);
|
|
28059
|
+
const run2 = await speaker.runs.start({ spaceId, goal: goal ?? goalOf(message, agent.config.name) });
|
|
28060
|
+
const turn = {
|
|
28061
|
+
run: run2,
|
|
28062
|
+
speaker,
|
|
28063
|
+
agent,
|
|
28064
|
+
who: speakerId ?? agent.agentId,
|
|
28065
|
+
hermesRun: "",
|
|
28066
|
+
reply: "",
|
|
28067
|
+
saidAt: 0,
|
|
28068
|
+
cancelled: false,
|
|
28069
|
+
next: [],
|
|
28070
|
+
open: /* @__PURE__ */ new Map(),
|
|
28071
|
+
counted: 0,
|
|
28072
|
+
approvals: /* @__PURE__ */ new Map(),
|
|
28073
|
+
allowed: [],
|
|
28074
|
+
interrupted: false
|
|
28075
|
+
};
|
|
28076
|
+
run2.on("cancel", () => {
|
|
28077
|
+
turn.cancelled = true;
|
|
28078
|
+
if (turn.hermesRun !== "") void agent.client.stop(turn.hermesRun).catch(() => void 0);
|
|
28079
|
+
});
|
|
28080
|
+
run2.on("steer", (steer) => {
|
|
28081
|
+
const words4 = `${steer.from?.display_name ?? "The person"} adds: ${steer.text}`;
|
|
28082
|
+
void this.steered(turn, words4, steer.mode);
|
|
28083
|
+
});
|
|
28084
|
+
let session;
|
|
28085
|
+
try {
|
|
28086
|
+
session = await this.sessionFor(room);
|
|
28087
|
+
} catch (error62) {
|
|
28088
|
+
await this.failed(turn, `${agent.config.name} couldn't open a session in Hermes: ${messageOf3(error62)}`, true);
|
|
28089
|
+
return;
|
|
28090
|
+
}
|
|
28091
|
+
const name2 = speakerId === void 0 ? agent.config.name : await this.nameOf(speakerId, agent.config.name);
|
|
28092
|
+
turn.folder = this.folderOf(agent, speakerId ?? agent.agentId, name2);
|
|
28093
|
+
const project = await this.projectFor(turn, room.spaceId, speakerId ?? agent.agentId, name2);
|
|
28094
|
+
let text = promptFor(message, { taker: speakerId, sayWho, project });
|
|
28095
|
+
for (let round = 0; text !== void 0 && !turn.cancelled && !run2.over; round += 1) {
|
|
28096
|
+
const last = await this.once(turn, session, text, `${run2.id}:${round}`);
|
|
28097
|
+
if (last === void 0) return;
|
|
28098
|
+
if (last.event === "run.cancelled" && turn.interrupted && turn.next.length > 0) {
|
|
28099
|
+
turn.interrupted = false;
|
|
28100
|
+
text = turn.next.shift();
|
|
28101
|
+
continue;
|
|
27694
28102
|
}
|
|
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 ?? "";
|
|
28103
|
+
if (last.event !== "run.completed") {
|
|
28104
|
+
await this.ended(turn, last);
|
|
28105
|
+
return;
|
|
27700
28106
|
}
|
|
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);
|
|
28107
|
+
text = turn.next.shift();
|
|
28108
|
+
if (text === void 0) await this.finish(turn, last);
|
|
27706
28109
|
}
|
|
27707
28110
|
}
|
|
27708
|
-
|
|
27709
|
-
|
|
27710
|
-
|
|
27711
|
-
|
|
27712
|
-
|
|
27713
|
-
|
|
27714
|
-
|
|
27715
|
-
|
|
27716
|
-
|
|
28111
|
+
/** One Hermes run of the turn: started, its events heard to the end. The end, or nothing on failure. */
|
|
28112
|
+
async once(turn, session, text, key) {
|
|
28113
|
+
const { agent } = turn;
|
|
28114
|
+
try {
|
|
28115
|
+
turn.hermesRun = await agent.client.startRun({
|
|
28116
|
+
text,
|
|
28117
|
+
sessionId: session,
|
|
28118
|
+
...this.runModel(turn),
|
|
28119
|
+
key,
|
|
28120
|
+
instructions: instructionsFor(turn.folder, turn.project !== void 0)
|
|
28121
|
+
});
|
|
28122
|
+
} catch (error62) {
|
|
28123
|
+
await this.failed(turn, `${agent.config.name} couldn't start in Hermes: ${messageOf3(error62)}`, true);
|
|
28124
|
+
return void 0;
|
|
28125
|
+
}
|
|
28126
|
+
if (turn.cancelled) await agent.client.stop(turn.hermesRun).catch(() => void 0);
|
|
28127
|
+
try {
|
|
28128
|
+
const last = await agent.client.events(turn.hermesRun, (event) => this.heard(turn, event));
|
|
28129
|
+
return last ?? await this.endOf(turn);
|
|
28130
|
+
} catch (error62) {
|
|
28131
|
+
this.log(`lost ${agent.config.name}'s run ${turn.hermesRun}: ${messageOf3(error62)}`);
|
|
28132
|
+
return await this.endOf(turn);
|
|
27717
28133
|
}
|
|
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
28134
|
}
|
|
27731
|
-
|
|
27732
|
-
|
|
27733
|
-
|
|
27734
|
-
|
|
27735
|
-
|
|
27736
|
-
|
|
27737
|
-
|
|
27738
|
-
|
|
27739
|
-
trouble = await this.restartOne(gateway);
|
|
27740
|
-
} catch (error62) {
|
|
27741
|
-
trouble = `Restart failed: ${messageOf3(error62)}`;
|
|
28135
|
+
/** The end of a run whose stream was lost: its state read until it is over, as a terminal event. */
|
|
28136
|
+
async endOf(turn) {
|
|
28137
|
+
const by = Date.now() + LOST_STREAM_MS;
|
|
28138
|
+
while (Date.now() < by) {
|
|
28139
|
+
const state2 = await turn.agent.client.run(turn.hermesRun).catch(() => void 0);
|
|
28140
|
+
const status = typeof state2?.["status"] === "string" ? state2["status"] : void 0;
|
|
28141
|
+
if (status !== void 0 && TERMINAL.has(`run.${status}`)) {
|
|
28142
|
+
return { ...state2, event: `run.${status}` };
|
|
27742
28143
|
}
|
|
27743
|
-
|
|
28144
|
+
await new Promise((resolve7) => setTimeout(resolve7, 1e3));
|
|
27744
28145
|
}
|
|
27745
|
-
|
|
28146
|
+
await this.failed(turn, `${turn.agent.config.name}'s run in Hermes stopped answering.`, true);
|
|
28147
|
+
return void 0;
|
|
27746
28148
|
}
|
|
27747
|
-
/**
|
|
27748
|
-
async
|
|
27749
|
-
const
|
|
27750
|
-
|
|
27751
|
-
|
|
27752
|
-
|
|
28149
|
+
/** The room's session: the one it had, or a new one Hermes makes for it. */
|
|
28150
|
+
async sessionFor({ id: id2, agent, spaceId }) {
|
|
28151
|
+
const kept = this.options.state.sessions[id2];
|
|
28152
|
+
if (kept !== void 0) return kept;
|
|
28153
|
+
const who = id2.slice(0, id2.lastIndexOf("|"));
|
|
28154
|
+
const made = await agent.client.newSession(`Chips space ${spaceId}${who === agent.key ? "" : ` \xB7 ${who}`}`);
|
|
28155
|
+
this.options.state.sessions[id2] = made;
|
|
28156
|
+
this.options.saveState(this.options.state);
|
|
28157
|
+
return made;
|
|
28158
|
+
}
|
|
28159
|
+
/**
|
|
28160
|
+
* A person's words for a run that is going. A steer is said to the Hermes run, or to the next one
|
|
28161
|
+
* of the turn when Hermes won't take it now. A follow-up waits for this one to end. An
|
|
28162
|
+
* interrupt stops it, and the turn goes on with the words.
|
|
28163
|
+
*/
|
|
28164
|
+
async steered(turn, text, mode) {
|
|
28165
|
+
if (mode === "interrupt" && turn.hermesRun !== "") {
|
|
28166
|
+
turn.next.unshift(text);
|
|
28167
|
+
turn.interrupted = true;
|
|
28168
|
+
await turn.agent.client.stop(turn.hermesRun).catch(() => void 0);
|
|
28169
|
+
return;
|
|
28170
|
+
}
|
|
28171
|
+
if (mode === "steer" && turn.hermesRun !== "") {
|
|
27753
28172
|
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);
|
|
28173
|
+
await turn.agent.client.steer(turn.hermesRun, text);
|
|
28174
|
+
return;
|
|
27769
28175
|
} catch (error62) {
|
|
27770
|
-
|
|
28176
|
+
this.log(`Hermes didn't take a steer mid-run (${messageOf3(error62)}); saying it next`);
|
|
27771
28177
|
}
|
|
27772
28178
|
}
|
|
28179
|
+
turn.next.push(text);
|
|
28180
|
+
}
|
|
28181
|
+
// ------------------------------------------------------------------ what Hermes streams
|
|
28182
|
+
async heard(turn, event) {
|
|
28183
|
+
if (turn.run.over) return;
|
|
28184
|
+
switch (event.event) {
|
|
28185
|
+
case "message.delta":
|
|
28186
|
+
turn.reply += typeof event["delta"] === "string" ? event["delta"] : "";
|
|
28187
|
+
return;
|
|
28188
|
+
case "message.interim":
|
|
28189
|
+
case "reasoning.available":
|
|
28190
|
+
this.sayThought(turn, String(event["text"] ?? ""), event.event === "message.interim" ? "writing" : "thinking");
|
|
28191
|
+
return;
|
|
28192
|
+
case "tool.started":
|
|
28193
|
+
case "subagent.start":
|
|
28194
|
+
await this.started(turn, event);
|
|
28195
|
+
return;
|
|
28196
|
+
case "tool.completed":
|
|
28197
|
+
case "subagent.complete":
|
|
28198
|
+
await this.completed(turn, event);
|
|
28199
|
+
return;
|
|
28200
|
+
case "approval.request":
|
|
28201
|
+
void this.ask(turn, event);
|
|
28202
|
+
return;
|
|
28203
|
+
case "approval.responded":
|
|
28204
|
+
await this.settled(turn, event);
|
|
28205
|
+
return;
|
|
28206
|
+
default:
|
|
28207
|
+
return;
|
|
28208
|
+
}
|
|
27773
28209
|
}
|
|
27774
|
-
|
|
27775
|
-
|
|
27776
|
-
|
|
27777
|
-
|
|
27778
|
-
|
|
27779
|
-
|
|
27780
|
-
return serviceOf(this.options.system, this.options.root, this.options.env);
|
|
28210
|
+
sayThought(turn, text, phase) {
|
|
28211
|
+
const now = Date.now();
|
|
28212
|
+
const line2 = lastLine(text);
|
|
28213
|
+
if (line2 === "" || now - turn.saidAt < QUIET_MS) return;
|
|
28214
|
+
turn.saidAt = now;
|
|
28215
|
+
void turn.run.status({ phase, verbPhrase: line2.slice(0, 200) }).catch(() => void 0);
|
|
27781
28216
|
}
|
|
27782
|
-
async
|
|
27783
|
-
const
|
|
27784
|
-
|
|
27785
|
-
|
|
27786
|
-
|
|
27787
|
-
const
|
|
27788
|
-
const
|
|
27789
|
-
|
|
27790
|
-
|
|
27791
|
-
|
|
27792
|
-
|
|
27793
|
-
|
|
27794
|
-
|
|
27795
|
-
|
|
27796
|
-
|
|
28217
|
+
async started(turn, event) {
|
|
28218
|
+
const sub = event.event === "subagent.start";
|
|
28219
|
+
const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
|
|
28220
|
+
turn.counted += 1;
|
|
28221
|
+
const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
|
|
28222
|
+
const stepId = callId ?? `${tool}-${turn.counted}`;
|
|
28223
|
+
const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
|
|
28224
|
+
turn.open.set(lane, [...turn.open.get(lane) ?? [], stepId]);
|
|
28225
|
+
const said2 = sub ? event["goal"] : event["preview"];
|
|
28226
|
+
await turn.run.step({
|
|
28227
|
+
stepId,
|
|
28228
|
+
status: "started",
|
|
28229
|
+
title: `${tool}${typeof said2 === "string" && said2 !== "" ? ` \xB7 ${said2}` : ""}`.slice(0, 300),
|
|
28230
|
+
tool,
|
|
28231
|
+
...KINDS[tool] === void 0 ? {} : { kind: KINDS[tool] }
|
|
28232
|
+
}).catch(() => void 0);
|
|
28233
|
+
}
|
|
28234
|
+
async completed(turn, event) {
|
|
28235
|
+
const sub = event.event === "subagent.complete";
|
|
28236
|
+
const tool = sub ? "delegate_task" : String(event["tool"] ?? "tool");
|
|
28237
|
+
const callId = typeof event["tool_call_id"] === "string" ? event["tool_call_id"] : void 0;
|
|
28238
|
+
const lane = callId ?? (sub ? "subagent" : `tool:${tool}`);
|
|
28239
|
+
const waiting = turn.open.get(lane) ?? [];
|
|
28240
|
+
const stepId = callId ?? waiting.shift() ?? `${tool}-${turn.counted += 1}`;
|
|
28241
|
+
turn.open.set(lane, waiting.filter((each) => each !== stepId));
|
|
28242
|
+
const failed = event["error"] === true || sub && event["status"] === "failed";
|
|
28243
|
+
const approvalId = sub ? void 0 : turn.allowed.shift();
|
|
28244
|
+
if (approvalId !== void 0) {
|
|
28245
|
+
await turn.run.executed(approvalId, failed ? "failed" : "executed").catch(() => void 0);
|
|
27797
28246
|
}
|
|
27798
|
-
const
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
27803
|
-
|
|
28247
|
+
const summary = sub ? event["summary"] : resultWords(event["preview"]);
|
|
28248
|
+
const seconds = typeof event["duration"] === "number" ? event["duration"] : void 0;
|
|
28249
|
+
await turn.run.step({
|
|
28250
|
+
stepId,
|
|
28251
|
+
status: failed ? "failed" : "done",
|
|
28252
|
+
tool,
|
|
28253
|
+
...typeof summary === "string" && summary !== "" ? { summary: summary.slice(0, 2e3) } : {},
|
|
28254
|
+
...seconds === void 0 ? {} : { durationMs: Math.round(seconds * 1e3) }
|
|
28255
|
+
}).catch(() => void 0);
|
|
28256
|
+
const cost3 = event["cost_usd"];
|
|
28257
|
+
if (sub && typeof cost3 === "number" && cost3 > 0) {
|
|
28258
|
+
const model = typeof event["model"] === "string" ? event["model"] : "subagent";
|
|
28259
|
+
await turn.run.usage({ scope: "step", model, costUsd: cost3 }).catch(() => void 0);
|
|
27804
28260
|
}
|
|
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
28261
|
}
|
|
27810
|
-
|
|
27811
|
-
|
|
27812
|
-
|
|
28262
|
+
// ------------------------------------------------------------------ what Hermes asks
|
|
28263
|
+
/** An approval Hermes wants before it acts, as a card; the answer is Hermes's choice. */
|
|
28264
|
+
async ask(turn, event) {
|
|
28265
|
+
const requestId = idOf(event);
|
|
28266
|
+
const command = typeof event["command"] === "string" ? event["command"] : void 0;
|
|
28267
|
+
const description = typeof event["description"] === "string" ? event["description"] : void 0;
|
|
28268
|
+
const choices = Array.isArray(event["choices"]) ? event["choices"].map(String) : ["once", "deny"];
|
|
28269
|
+
const approvalId = ids.next("approval");
|
|
28270
|
+
turn.approvals.set(requestId ?? approvalId, { approvalId, answered: false });
|
|
28271
|
+
const asked = turn.run.approval({
|
|
28272
|
+
approvalId,
|
|
28273
|
+
action: {
|
|
28274
|
+
kind: command === void 0 ? "hermes.approval" : "shell.exec",
|
|
28275
|
+
input: { ...command === void 0 ? {} : { command }, ...description === void 0 ? {} : { description } }
|
|
28276
|
+
},
|
|
28277
|
+
question: (description === void 0 ? "Run this command?" : `Allow it to ${lowerFirst(description)}?`).slice(0, 300),
|
|
28278
|
+
lasts: "once",
|
|
28279
|
+
risk: [],
|
|
28280
|
+
...command === void 0 ? {} : { preview: { format: "markdown", title: "Run a command", body: `\`\`\`sh
|
|
28281
|
+
${command}
|
|
28282
|
+
\`\`\`` } }
|
|
28283
|
+
});
|
|
28284
|
+
const stopped2 = new Promise((resolve7) => turn.run.on("cancel", () => resolve7(void 0)));
|
|
28285
|
+
const decision = await Promise.race([asked, stopped2]);
|
|
28286
|
+
const pending = turn.approvals.get(requestId ?? approvalId);
|
|
28287
|
+
if (decision === void 0 || pending === void 0 || pending.answered) return;
|
|
28288
|
+
pending.answered = true;
|
|
28289
|
+
if (decision.allowed) turn.allowed.push(approvalId);
|
|
28290
|
+
const generation = event["execution_generation"];
|
|
28291
|
+
await turn.agent.client.approve(turn.hermesRun, {
|
|
28292
|
+
choice: choiceOf(decision, choices, command !== void 0),
|
|
28293
|
+
...requestId === void 0 ? {} : { request_id: requestId },
|
|
28294
|
+
...typeof generation === "number" ? { execution_generation: generation } : {}
|
|
28295
|
+
}).catch((error62) => this.log(`Hermes didn't take the answer: ${messageOf3(error62)}`));
|
|
27813
28296
|
}
|
|
27814
|
-
/**
|
|
27815
|
-
async
|
|
27816
|
-
const
|
|
27817
|
-
|
|
27818
|
-
if (
|
|
27819
|
-
|
|
27820
|
-
|
|
27821
|
-
|
|
27822
|
-
|
|
27823
|
-
|
|
27824
|
-
|
|
28297
|
+
/** An approval answered in Hermes's own tools: settled in Chips, once, so the card cools (ADR 0079). */
|
|
28298
|
+
async settled(turn, event) {
|
|
28299
|
+
const requestId = idOf(event);
|
|
28300
|
+
const pending = requestId === void 0 ? void 0 : turn.approvals.get(requestId);
|
|
28301
|
+
if (pending === void 0 || pending.answered) return;
|
|
28302
|
+
pending.answered = true;
|
|
28303
|
+
const denied = event["choice"] === "deny";
|
|
28304
|
+
await this.options.chips.send(
|
|
28305
|
+
"approval.resolved",
|
|
28306
|
+
{ run_id: turn.run.id },
|
|
28307
|
+
{
|
|
28308
|
+
approval_id: pending.approvalId,
|
|
28309
|
+
decision: denied ? "denied" : "approved",
|
|
28310
|
+
scope: "once",
|
|
28311
|
+
reason: "Answered in Hermes."
|
|
28312
|
+
},
|
|
28313
|
+
`hermes-approval:${turn.hermesRun}:${requestId}:settled`
|
|
28314
|
+
).catch(() => void 0);
|
|
27825
28315
|
}
|
|
27826
|
-
|
|
27827
|
-
|
|
27828
|
-
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
28316
|
+
// ------------------------------------------------------------------ the end
|
|
28317
|
+
/** The turn is over: its words in the room, its tokens, and the run ended. */
|
|
28318
|
+
async finish(turn, last) {
|
|
28319
|
+
const { run: run2, agent } = turn;
|
|
28320
|
+
if (run2.over) return;
|
|
28321
|
+
await this.closeSteps(turn, "done");
|
|
28322
|
+
for (const approvalId of turn.allowed.splice(0)) {
|
|
28323
|
+
await run2.executed(approvalId, "executed", "Hermes didn't say how it went.").catch(() => void 0);
|
|
27832
28324
|
}
|
|
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);
|
|
28325
|
+
await this.counted(turn, last);
|
|
28326
|
+
const output2 = typeof last["output"] === "string" ? last["output"].trim() : "";
|
|
28327
|
+
const answer = output2 !== "" ? output2 : turn.reply.trim();
|
|
28328
|
+
const { change, note } = await this.sendProject(turn, answer);
|
|
28329
|
+
const { folder: folder2 } = turn;
|
|
28330
|
+
const { chips: chips2 } = this.options;
|
|
28331
|
+
const words4 = [answer, note ?? ""].filter((part) => part !== "").join("\n\n");
|
|
28332
|
+
const post = await sendAnswer(words4, {
|
|
28333
|
+
folders: folder2 === void 0 ? [] : [folder2],
|
|
28334
|
+
upload: (input2) => chips2.upload(input2)
|
|
28335
|
+
});
|
|
28336
|
+
const reply = post.text;
|
|
28337
|
+
if (reply !== "" || post.uploadIds.length > 0) {
|
|
28338
|
+
await turn.speaker.post({ spaceId: run2.spaceId, runId: run2.id, text: reply, uploadIds: post.uploadIds, change });
|
|
27852
28339
|
}
|
|
28340
|
+
const said2 = post.summary;
|
|
28341
|
+
const summary = said2 === "" ? "Done." : said2.length <= 2e3 ? said2 : `${said2.slice(0, 1990)}\u2026`;
|
|
28342
|
+
await run2.complete({ summary });
|
|
28343
|
+
this.log(`${agent.config.name} answered in ${run2.spaceId}`);
|
|
28344
|
+
this.missingReadings();
|
|
27853
28345
|
}
|
|
27854
28346
|
/**
|
|
27855
|
-
*
|
|
27856
|
-
*
|
|
28347
|
+
* A turn ended and Hermes sent no reading at all since the bridge started: its gateway most
|
|
28348
|
+
* likely started before Chips's webhook was in its config. Said once, a little after, since a
|
|
28349
|
+
* reading comes a moment after its model call.
|
|
27857
28350
|
*/
|
|
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 };
|
|
28351
|
+
missingReadings() {
|
|
28352
|
+
if (this.fills === void 0 || this.fillsPort === void 0 || this.fills.anyHeard || this.toldNoReadings) return;
|
|
28353
|
+
this.toldNoReadings = true;
|
|
28354
|
+
setTimeout(() => {
|
|
28355
|
+
if (this.fills?.anyHeard === false) {
|
|
28356
|
+
this.log(
|
|
28357
|
+
"context window: Hermes sent no reading after a turn. Start hermes gateway again once, so it takes Chips's webhook from its config."
|
|
28358
|
+
);
|
|
27906
28359
|
}
|
|
27907
|
-
|
|
27908
|
-
|
|
27909
|
-
|
|
28360
|
+
}, 1e4).unref();
|
|
28361
|
+
}
|
|
28362
|
+
/** A run that failed, was stopped or was cut short in Hermes. */
|
|
28363
|
+
async ended(turn, last) {
|
|
28364
|
+
await this.closeSteps(turn, "failed");
|
|
28365
|
+
await this.counted(turn, last);
|
|
28366
|
+
if (turn.cancelled || turn.run.over) return;
|
|
28367
|
+
const name2 = turn.agent.config.name;
|
|
28368
|
+
if (last.event === "run.cancelled") await this.failed(turn, `${name2}'s run was stopped in Hermes.`, false);
|
|
28369
|
+
else if (last.event === "run.interrupted") await this.failed(turn, `Hermes restarted during ${name2}'s run.`, true);
|
|
28370
|
+
else {
|
|
28371
|
+
const why = typeof last["error"] === "string" && last["error"] !== "" ? last["error"] : "it failed in Hermes";
|
|
28372
|
+
await this.failed(turn, `${name2}: ${why}`, true);
|
|
27910
28373
|
}
|
|
27911
28374
|
}
|
|
27912
|
-
|
|
27913
|
-
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
|
|
28375
|
+
/**
|
|
28376
|
+
* The tokens Hermes counted, as a step's share, so they add up over the turn's runs. Hermes's
|
|
28377
|
+
* `input_tokens` is already every token in, its cache's reads and writes among them (its
|
|
28378
|
+
* `CanonicalUsage.prompt_tokens`), which is what Chips counts (Chips ADR 0214); the cache's two
|
|
28379
|
+
* counts go with it, and are left out where Hermes doesn't say them.
|
|
28380
|
+
*/
|
|
28381
|
+
async counted(turn, last) {
|
|
28382
|
+
const usage2 = last["usage"] ?? {};
|
|
28383
|
+
const count = (key) => typeof usage2[key] === "number" ? usage2[key] : void 0;
|
|
28384
|
+
const input2 = count("input_tokens") ?? 0;
|
|
28385
|
+
const output2 = count("output_tokens") ?? 0;
|
|
28386
|
+
if (input2 + output2 === 0) return;
|
|
28387
|
+
const runtime = last["runtime"] ?? {};
|
|
28388
|
+
const model = typeof runtime["model"] === "string" ? runtime["model"] : this.runModel(turn).model ?? "hermes";
|
|
28389
|
+
await turn.run.usage({
|
|
28390
|
+
scope: "step",
|
|
28391
|
+
model,
|
|
28392
|
+
inputTokens: input2,
|
|
28393
|
+
cacheReadTokens: count("cache_read_tokens"),
|
|
28394
|
+
cacheWriteTokens: count("cache_write_tokens"),
|
|
28395
|
+
outputTokens: output2
|
|
28396
|
+
}).catch(() => void 0);
|
|
28397
|
+
}
|
|
28398
|
+
async closeSteps(turn, status) {
|
|
28399
|
+
for (const [, ids2] of turn.open) {
|
|
28400
|
+
for (const stepId of ids2) await turn.run.step({ stepId, status }).catch(() => void 0);
|
|
27919
28401
|
}
|
|
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
|
-
};
|
|
28402
|
+
turn.open.clear();
|
|
28403
|
+
}
|
|
28404
|
+
async failed(turn, error62, retryable) {
|
|
28405
|
+
if (!turn.run.over) await turn.run.fail({ error: error62, retryable }).catch(() => void 0);
|
|
28406
|
+
if (retryable) void this.sayHowItIs();
|
|
28407
|
+
}
|
|
28408
|
+
};
|
|
28409
|
+
function choiceOf(decision, choices, command = false) {
|
|
28410
|
+
if (!decision.allowed) return "deny";
|
|
28411
|
+
if (command) return "once";
|
|
28412
|
+
if (decision.scope === "always" && choices.includes("always")) return "always";
|
|
28413
|
+
if (decision.scope !== "once" && choices.includes("session")) return "session";
|
|
28414
|
+
return "once";
|
|
27942
28415
|
}
|
|
27943
|
-
function
|
|
27944
|
-
|
|
28416
|
+
function idOf(event) {
|
|
28417
|
+
const id2 = event["request_id"] ?? event["prompt_id"];
|
|
28418
|
+
return typeof id2 === "string" && id2 !== "" ? id2 : void 0;
|
|
27945
28419
|
}
|
|
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
|
-
});
|
|
28420
|
+
function messageOf3(error62) {
|
|
28421
|
+
if (error62 instanceof HermesError) return `${error62.message} (${error62.status})`;
|
|
28422
|
+
return error62 instanceof Error ? error62.message : String(error62);
|
|
27965
28423
|
}
|
|
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
|
-
};
|
|
28424
|
+
function titleFrom(text) {
|
|
28425
|
+
const line2 = text.trim().split("\n")[0] ?? "";
|
|
28426
|
+
if (line2.length <= 48) return line2;
|
|
28427
|
+
const cut2 = line2.slice(0, 48);
|
|
28428
|
+
return `${cut2.slice(0, Math.max(cut2.lastIndexOf(" "), 24))}\u2026`;
|
|
28429
|
+
}
|
|
28430
|
+
function forkName(original) {
|
|
28431
|
+
return `${original.trim().replace(/ · fork( \d+)?$/, "")} \xB7 fork`;
|
|
28432
|
+
}
|
|
28433
|
+
function goalOf(message, name2) {
|
|
28434
|
+
const first = message.text.split("\n").find((line2) => line2.trim() !== "")?.trim() ?? "";
|
|
28435
|
+
const named = first.toLowerCase().startsWith(`@${name2.toLowerCase()}`);
|
|
28436
|
+
const goal = named ? first.slice(name2.length + 1).trim() : first.replace(/^@\S+\s*/, "");
|
|
28437
|
+
return (goal === "" ? `Answer ${message.from.display_name}` : goal).slice(0, 300);
|
|
28438
|
+
}
|
|
28439
|
+
var REPLY_SHOWN = 1500;
|
|
28440
|
+
var GOAL_SHOWN = 120;
|
|
28441
|
+
function lookAgain(who, reply, note) {
|
|
28442
|
+
const said2 = reply?.trim() ?? "";
|
|
28443
|
+
return [
|
|
28444
|
+
`${who} says your reply in this space wasn't right.`,
|
|
28445
|
+
...said2 === "" ? [] : [`Your reply:
|
|
28446
|
+
\xAB${cut(said2, REPLY_SHOWN)}\xBB`],
|
|
28447
|
+
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`,
|
|
28448
|
+
"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."
|
|
28449
|
+
].join("\n\n");
|
|
28450
|
+
}
|
|
28451
|
+
function lookingAgain(note) {
|
|
28452
|
+
const first = note?.split("\n").find((line2) => line2.trim() !== "")?.trim();
|
|
28453
|
+
return first === void 0 ? "Looking again at its reply" : cut(`Looking again: ${first}`, GOAL_SHOWN);
|
|
28454
|
+
}
|
|
28455
|
+
function cut(text, most) {
|
|
28456
|
+
return text.length <= most ? text : `${text.slice(0, most - 1)}\u2026`;
|
|
28457
|
+
}
|
|
28458
|
+
function resultWords(preview2) {
|
|
28459
|
+
if (typeof preview2 !== "string" || !preview2.trimStart().startsWith("{")) return preview2;
|
|
28460
|
+
let result;
|
|
28461
|
+
try {
|
|
28462
|
+
result = JSON.parse(preview2);
|
|
28463
|
+
} catch {
|
|
28464
|
+
return preview2;
|
|
28465
|
+
}
|
|
28466
|
+
if (typeof result !== "object" || result === null || !("output" in result)) return preview2;
|
|
28467
|
+
const { output: output2, error: error62 } = result;
|
|
28468
|
+
const said2 = [output2, error62].find((each) => typeof each === "string" && each.trim() !== "");
|
|
28469
|
+
return typeof said2 === "string" ? said2.trim() : "";
|
|
28470
|
+
}
|
|
28471
|
+
function lastLine(text) {
|
|
28472
|
+
const lines2 = text.split(/(?<=[.!?])\s+|\n+/).map((line2) => line2.trim()).filter((line2) => line2 !== "");
|
|
28473
|
+
return lines2.at(-1) ?? "";
|
|
28474
|
+
}
|
|
28475
|
+
function lowerFirst(text) {
|
|
28476
|
+
return text.length === 0 ? text : `${text[0]?.toLowerCase() ?? ""}${text.slice(1)}`;
|
|
28066
28477
|
}
|
|
28067
28478
|
|
|
28068
28479
|
// src/keep.ts
|
|
28069
28480
|
import { homedir as homedir6 } from "node:os";
|
|
28070
|
-
import { basename as basename5, join as
|
|
28481
|
+
import { basename as basename5, join as join11, resolve as resolve4 } from "node:path";
|
|
28071
28482
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
28072
28483
|
var flags = process.argv.slice(2);
|
|
28073
28484
|
var words3 = flags.filter((word) => !word.startsWith("--"));
|
|
28074
|
-
var whole = (word) => resolve4(word.startsWith("~/") ?
|
|
28485
|
+
var whole = (word) => resolve4(word.startsWith("~/") ? join11(homedir6(), word.slice(2)) : word);
|
|
28075
28486
|
var here = fileURLToPath2(new URL("..", import.meta.url)).replace(/\/$/, "");
|
|
28076
28487
|
var self = fileURLToPath2(import.meta.url);
|
|
28077
28488
|
var run = self.endsWith(".js") ? [self] : ["--import", "tsx", "src/main.ts"];
|
|
@@ -28117,9 +28528,9 @@ function keepIfAsked(label2, url2, names, log2, before = () => void 0) {
|
|
|
28117
28528
|
}
|
|
28118
28529
|
|
|
28119
28530
|
// src/setup.ts
|
|
28120
|
-
import { existsSync as
|
|
28531
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "node:fs";
|
|
28121
28532
|
import { homedir as homedir7 } from "node:os";
|
|
28122
|
-
import { join as
|
|
28533
|
+
import { join as join12, resolve as resolve5 } from "node:path";
|
|
28123
28534
|
var SetupError = class extends Error {
|
|
28124
28535
|
constructor(message, words4 = {}) {
|
|
28125
28536
|
super(message);
|
|
@@ -28144,16 +28555,16 @@ function readyHooks(config3, options) {
|
|
|
28144
28555
|
const lines2 = [];
|
|
28145
28556
|
let restart2 = false;
|
|
28146
28557
|
const done = /* @__PURE__ */ new Set();
|
|
28147
|
-
for (const
|
|
28148
|
-
const profile = profileName(
|
|
28558
|
+
for (const named of [...config3.agents.map((agent) => agent.address.profile), ...options.made ?? []]) {
|
|
28559
|
+
const profile = profileName(named);
|
|
28149
28560
|
if (done.has(profile)) continue;
|
|
28150
28561
|
done.add(profile);
|
|
28151
|
-
const home2 = profileHome(options.root,
|
|
28562
|
+
const home2 = profileHome(options.root, named);
|
|
28152
28563
|
let outcome;
|
|
28153
28564
|
try {
|
|
28154
28565
|
outcome = addHook(home2, options.port, profile, options.secret);
|
|
28155
28566
|
} catch (error62) {
|
|
28156
|
-
lines2.push(`Chips's webhook wasn't added to ${shown(
|
|
28567
|
+
lines2.push(`Chips's webhook wasn't added to ${shown(join12(home2, "config.yaml"))}: ${error62 instanceof Error ? error62.message : String(error62)}`);
|
|
28157
28568
|
continue;
|
|
28158
28569
|
}
|
|
28159
28570
|
if (outcome.state === "added") {
|
|
@@ -28179,8 +28590,8 @@ function hermesEnvPath(env = process.env) {
|
|
|
28179
28590
|
}
|
|
28180
28591
|
function fromHermes(name2, env) {
|
|
28181
28592
|
const path = hermesEnvPath(env);
|
|
28182
|
-
if (!
|
|
28183
|
-
for (const line2 of
|
|
28593
|
+
if (!existsSync10(path)) return void 0;
|
|
28594
|
+
for (const line2 of readFileSync10(path, "utf8").split("\n")) {
|
|
28184
28595
|
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(line2);
|
|
28185
28596
|
if (match?.[1] === name2) return match[2]?.replace(/^(["'])(.*)\1$/, "$2");
|
|
28186
28597
|
}
|
|
@@ -28191,7 +28602,7 @@ function folderOf(said2) {
|
|
|
28191
28602
|
return resolve5(said2 === "~" || said2.startsWith("~/") ? homedir7() + said2.slice(1) : said2);
|
|
28192
28603
|
}
|
|
28193
28604
|
function readConfig(path, env = process.env) {
|
|
28194
|
-
const raw =
|
|
28605
|
+
const raw = existsSync10(path) ? JSON.parse(readFileSync10(path, "utf8")) : PLAIN;
|
|
28195
28606
|
const url2 = typeof raw.url === "string" ? raw.url : "http://127.0.0.1:8642";
|
|
28196
28607
|
const folder2 = folderOf(raw.folder);
|
|
28197
28608
|
if (!Array.isArray(raw.agents) || raw.agents.length === 0) {
|
|
@@ -28262,14 +28673,14 @@ async function checkHermes(config3, clientFor = (agent) => new HermesClient(agen
|
|
|
28262
28673
|
}
|
|
28263
28674
|
|
|
28264
28675
|
// src/main.ts
|
|
28265
|
-
var home = (path) => path.startsWith("~/") ?
|
|
28676
|
+
var home = (path) => path.startsWith("~/") ? join13(homedir8(), path.slice(2)) : path;
|
|
28266
28677
|
var link = process.env["CHIPS_JOIN_LINK"] || void 0;
|
|
28267
28678
|
var LABEL = "chat.chips.hermes";
|
|
28268
28679
|
stopIfAsked(LABEL);
|
|
28269
28680
|
var configPath = resolve6(home(words3[0] ?? "~/.chips-hermes/config.json"));
|
|
28270
28681
|
var folder = dirname6(configPath);
|
|
28271
|
-
var statePath =
|
|
28272
|
-
var state =
|
|
28682
|
+
var statePath = join13(folder, "sessions.json");
|
|
28683
|
+
var state = existsSync11(statePath) ? JSON.parse(readFileSync11(statePath, "utf8")) : { sessions: {} };
|
|
28273
28684
|
var saveState = (next) => {
|
|
28274
28685
|
mkdirSync6(folder, { recursive: true, mode: 448 });
|
|
28275
28686
|
writeFileSync5(statePath, `${JSON.stringify(next, null, 2)}
|
|
@@ -28294,12 +28705,13 @@ try {
|
|
|
28294
28705
|
reportStep("checking", { detail: "Looking for its API server" });
|
|
28295
28706
|
await checkHermes(config2);
|
|
28296
28707
|
}
|
|
28297
|
-
fills.secret ??=
|
|
28708
|
+
fills.secret ??= randomBytes3(32).toString("hex");
|
|
28298
28709
|
fills.port = config2.hookPort ?? fills.port ?? hookPortIn(profileHome(root, config2.agents[0]?.address.profile)) ?? await freePort();
|
|
28299
28710
|
saveState(state);
|
|
28300
28711
|
const first = config2.agents[0];
|
|
28301
28712
|
const gatewayUp = link !== void 0 || first !== void 0 && await new HermesClient(first.address).healthy();
|
|
28302
|
-
const
|
|
28713
|
+
const made = Object.values(state.made ?? {});
|
|
28714
|
+
const hooks = readyHooks(config2, { root, port: fills.port, secret: fills.secret, gatewayUp, made });
|
|
28303
28715
|
for (const line2 of hooks.lines) log(line2);
|
|
28304
28716
|
if (link !== void 0) reportStep("checked", { detail: checkedWords(config2, hooks.restart) });
|
|
28305
28717
|
} catch (error62) {
|
|
@@ -28348,7 +28760,7 @@ var chips = await connect({
|
|
|
28348
28760
|
url: joined.url,
|
|
28349
28761
|
token: joined.token,
|
|
28350
28762
|
client: { name: "chips-hermes", version: "1.0.0" },
|
|
28351
|
-
store: fileStore(
|
|
28763
|
+
store: fileStore(join13(folder, "chips-state.json")),
|
|
28352
28764
|
onRestart: restart,
|
|
28353
28765
|
settings: settingsFor(() => bridge)
|
|
28354
28766
|
});
|
|
@@ -28364,7 +28776,10 @@ bridge = new Bridge({
|
|
|
28364
28776
|
saveState,
|
|
28365
28777
|
log,
|
|
28366
28778
|
hermesRoot: root,
|
|
28367
|
-
fills: { port: fills.port, secret: fills.secret }
|
|
28779
|
+
fills: { port: fills.port, secret: fills.secret },
|
|
28780
|
+
// Each agent made in Chips is a Hermes profile of its own (Chips ADR 0241).
|
|
28781
|
+
profiles: hermesProfiles({ root, env: process.env, system: thisMachine(), log: (line2) => log(`profiles: ${line2}`) }),
|
|
28782
|
+
restartGateway: () => restart({ askedBy: "chips-hermes, for a new agent's profile", askedAt: (/* @__PURE__ */ new Date()).toISOString() })
|
|
28368
28783
|
});
|
|
28369
28784
|
await bridge.start();
|
|
28370
28785
|
log(`answering for ${config2.agents.map((agent) => agent.name).join(", ")} through Hermes`);
|