@saleso.innovations/bridge 0.1.38 → 0.1.40
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/INTEGRATION.md +14 -3
- package/dist/client.d.ts +32 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +21 -4
- package/dist/hermesCommands.d.ts +1 -1
- package/dist/hermesCommands.d.ts.map +1 -1
- package/dist/hermesCommands.js +7 -0
- package/dist/hermesForwarder.d.ts.map +1 -1
- package/dist/hermesForwarder.js +29 -67
- package/dist/renameHermesSession.d.ts +7 -0
- package/dist/renameHermesSession.d.ts.map +1 -0
- package/dist/renameHermesSession.js +39 -0
- package/dist/renameHermesSession.test.d.ts +2 -0
- package/dist/renameHermesSession.test.d.ts.map +1 -0
- package/dist/renameHermesSession.test.js +15 -0
- package/dist/turnActivity.d.ts +36 -0
- package/dist/turnActivity.d.ts.map +1 -0
- package/dist/turnActivity.js +233 -0
- package/dist/turnActivity.test.d.ts +2 -0
- package/dist/turnActivity.test.d.ts.map +1 -0
- package/dist/turnActivity.test.js +69 -0
- package/package.json +2 -2
package/INTEGRATION.md
CHANGED
|
@@ -34,10 +34,21 @@ export async function onCleosPairingCodeSubmitted(code: string) {
|
|
|
34
34
|
for await (const chunk of stream) {
|
|
35
35
|
reply.delta(chunk, sequence++);
|
|
36
36
|
}
|
|
37
|
-
// If you stream from Hermes /v1/chat/completions, also forward
|
|
38
|
-
// reply.activity() when you receive
|
|
37
|
+
// If you stream from Hermes /v1/chat/completions, also forward activity via
|
|
38
|
+
// reply.activity() when you receive progress SSE events:
|
|
39
|
+
// - `event: hermes.tool.progress` { tool, label?, emoji?, source?, server? }
|
|
40
|
+
// - `event: hermes.skill.progress` { skill, label?, emoji? }
|
|
41
|
+
// - `event: hermes.memory.progress` { memory, label?, emoji? }
|
|
42
|
+
// - `event: hermes.mcp.progress` { server, tool?, label?, emoji? }
|
|
39
43
|
// Do not inject tool markers into assistant text (see Hermes #6972).
|
|
40
|
-
|
|
44
|
+
// Accumulate the capabilities used during the turn and pass a `turnActivity`
|
|
45
|
+
// summary (plus the model that produced the reply) to reply.complete() so Cleos
|
|
46
|
+
// can show it on the completed message. The `forwardToHermes` helper does this
|
|
47
|
+
// automatically when you use createHermesMessageHandler().
|
|
48
|
+
reply.complete(stream.finalText, sequence, undefined, undefined, {
|
|
49
|
+
tools: [{ name: "web_search", count: 2 }],
|
|
50
|
+
model: "anthropic/claude-sonnet-4",
|
|
51
|
+
});
|
|
41
52
|
},
|
|
42
53
|
});
|
|
43
54
|
return session.agentId;
|
package/dist/client.d.ts
CHANGED
|
@@ -22,13 +22,44 @@ export type UserMessageMeta = {
|
|
|
22
22
|
replyMessageId: string;
|
|
23
23
|
attachments?: UserMessageAttachment[];
|
|
24
24
|
};
|
|
25
|
+
export type AgentActivityKind = "tool" | "skill" | "memory" | "mcp";
|
|
25
26
|
export type AgentActivityPayload = {
|
|
26
27
|
activityType: "tool.started" | "tool.completed";
|
|
28
|
+
kind?: AgentActivityKind;
|
|
27
29
|
tool?: string;
|
|
30
|
+
skill?: string;
|
|
31
|
+
memory?: string;
|
|
32
|
+
server?: string;
|
|
33
|
+
source?: string;
|
|
28
34
|
label?: string;
|
|
29
35
|
emoji?: string;
|
|
30
36
|
sequence: number;
|
|
31
37
|
};
|
|
38
|
+
/** Per-message summary of capabilities the agent used. Keep in sync with @repo/backend-types. */
|
|
39
|
+
export type AgentTurnActivity = {
|
|
40
|
+
tools?: Array<{
|
|
41
|
+
name: string;
|
|
42
|
+
label?: string;
|
|
43
|
+
emoji?: string;
|
|
44
|
+
count?: number;
|
|
45
|
+
source?: string;
|
|
46
|
+
}>;
|
|
47
|
+
skills?: Array<{
|
|
48
|
+
name: string;
|
|
49
|
+
label?: string;
|
|
50
|
+
}>;
|
|
51
|
+
memories?: Array<{
|
|
52
|
+
name: string;
|
|
53
|
+
label?: string;
|
|
54
|
+
}>;
|
|
55
|
+
mcps?: Array<{
|
|
56
|
+
server: string;
|
|
57
|
+
tool?: string;
|
|
58
|
+
label?: string;
|
|
59
|
+
count?: number;
|
|
60
|
+
}>;
|
|
61
|
+
model?: string;
|
|
62
|
+
};
|
|
32
63
|
export type AgentFailedPayload = {
|
|
33
64
|
error: string;
|
|
34
65
|
code?: string;
|
|
@@ -37,7 +68,7 @@ export type AgentFailedPayload = {
|
|
|
37
68
|
export type AgentReply = {
|
|
38
69
|
delta: (text: string, sequence: number) => void;
|
|
39
70
|
activity: (activity: AgentActivityPayload) => void;
|
|
40
|
-
complete: (text: string, sequence: number, attachments?: UserMessageAttachment[], hermesAssistantMessageId?: string) => void;
|
|
71
|
+
complete: (text: string, sequence: number, attachments?: UserMessageAttachment[], hermesAssistantMessageId?: string, turnActivity?: AgentTurnActivity) => void;
|
|
41
72
|
failed: (failure: AgentFailedPayload) => void;
|
|
42
73
|
};
|
|
43
74
|
export type CronDeliveryMeta = {
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAYhG,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrG,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,cAAc,GAAG,gBAAgB,CAAC;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,QAAQ,EAAE,CAAC,QAAQ,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACnD,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,qBAAqB,EAAE,EACrC,wBAAwB,CAAC,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -9,6 +9,13 @@ import { DEFAULT_BRIDGE_CAPABILITIES } from "./constants.js";
|
|
|
9
9
|
export function cronDeliveryMessageId(sourceKey) {
|
|
10
10
|
return `cron:${sourceKey}`;
|
|
11
11
|
}
|
|
12
|
+
function hasTurnActivity(activity) {
|
|
13
|
+
return ((activity.tools?.length ?? 0) > 0 ||
|
|
14
|
+
(activity.skills?.length ?? 0) > 0 ||
|
|
15
|
+
(activity.memories?.length ?? 0) > 0 ||
|
|
16
|
+
(activity.mcps?.length ?? 0) > 0 ||
|
|
17
|
+
typeof activity.model === "string");
|
|
18
|
+
}
|
|
12
19
|
function parseUserMessageAttachments(raw) {
|
|
13
20
|
if (!Array.isArray(raw))
|
|
14
21
|
return undefined;
|
|
@@ -71,13 +78,25 @@ function createReplySender(ws, agentId, conversationId, messageId) {
|
|
|
71
78
|
conversationId,
|
|
72
79
|
messageId,
|
|
73
80
|
activityType: activity.activityType,
|
|
81
|
+
kind: activity.kind,
|
|
74
82
|
tool: activity.tool,
|
|
83
|
+
skill: activity.skill,
|
|
84
|
+
memory: activity.memory,
|
|
85
|
+
server: activity.server,
|
|
86
|
+
source: activity.source,
|
|
75
87
|
label: activity.label,
|
|
76
88
|
emoji: activity.emoji,
|
|
77
89
|
sequence: activity.sequence,
|
|
78
90
|
}));
|
|
79
91
|
},
|
|
80
|
-
complete(text, sequence, attachments, hermesAssistantMessageId) {
|
|
92
|
+
complete(text, sequence, attachments, hermesAssistantMessageId, turnActivity) {
|
|
93
|
+
const metadata = {};
|
|
94
|
+
if (hermesAssistantMessageId) {
|
|
95
|
+
metadata.hermes = { assistantMessageId: hermesAssistantMessageId };
|
|
96
|
+
}
|
|
97
|
+
if (turnActivity && hasTurnActivity(turnActivity)) {
|
|
98
|
+
metadata.turnActivity = turnActivity;
|
|
99
|
+
}
|
|
81
100
|
ws.send(JSON.stringify({
|
|
82
101
|
type: "agent.message",
|
|
83
102
|
agentId,
|
|
@@ -87,9 +106,7 @@ function createReplySender(ws, agentId, conversationId, messageId) {
|
|
|
87
106
|
sequence,
|
|
88
107
|
final: true,
|
|
89
108
|
...(attachments && attachments.length > 0 ? { attachments } : {}),
|
|
90
|
-
...(
|
|
91
|
-
? { metadata: { hermes: { assistantMessageId: hermesAssistantMessageId } } }
|
|
92
|
-
: {}),
|
|
109
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
93
110
|
}));
|
|
94
111
|
},
|
|
95
112
|
failed(failure) {
|
package/dist/hermesCommands.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ShellDeltaEmitter } from "./shellSession.js";
|
|
2
|
-
export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create", "gateway.start", "gateway.stop", "gateway.restart", "hermes.update", "bridge.version", "bridge.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.usage.get", "sessions.list", "skills.list", "tools.list", "tools.set", "mcp.list", "mcp.add", "mcp.remove", "files.list", "files.read", "files.write", "files.delete", "memories.list", "shell.exec", "shell.session.reset"];
|
|
2
|
+
export declare const HERMES_COMMAND_NAMES: readonly ["runtime.health", "runtime.detailedHealth", "runtime.version", "runtime.capabilities", "models.list", "model.set", "responses.create", "runs.create", "runs.status", "runs.stop", "jobs.list", "jobs.get", "jobs.create", "jobs.update", "jobs.pause", "jobs.resume", "jobs.runNow", "jobs.delete", "profiles.list", "profiles.create", "gateway.start", "gateway.stop", "gateway.restart", "hermes.update", "bridge.version", "bridge.update", "sessions.messages.list", "sessions.messages.countSent", "sessions.titles.resolve", "sessions.rename", "sessions.usage.get", "sessions.list", "skills.list", "tools.list", "tools.set", "mcp.list", "mcp.add", "mcp.remove", "files.list", "files.read", "files.write", "files.delete", "memories.list", "shell.exec", "shell.session.reset"];
|
|
3
3
|
export type HermesCommandName = (typeof HERMES_COMMAND_NAMES)[number];
|
|
4
4
|
export declare function isHermesCommandName(value: string): value is HermesCommandName;
|
|
5
5
|
export type HermesCommandErrorCode = "command_unsupported" | "hermes_unreachable" | "hermes_request_failed" | "invalid_command_args" | "unsupported_by_http";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hermesCommands.d.ts","sourceRoot":"","sources":["../src/hermesCommands.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAuC,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAMhG,eAAO,MAAM,oBAAoB,ywBA8CvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,iBAAiB,CAE7E;AAED,MAAM,MAAM,sBAAsB,GAC9B,qBAAqB,GACrB,oBAAoB,GACpB,uBAAuB,GACvB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;gBAE1B,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,MAAM;CAI1D;AAkID,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAAO,GAC9F,OAAO,CAAC,OAAO,CAAC,CAuQlB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,sBAAsB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAetG"}
|
package/dist/hermesCommands.js
CHANGED
|
@@ -7,6 +7,7 @@ import { listHermesTools, setHermesToolEnabled } from "./toolsList.js";
|
|
|
7
7
|
import { addHermesMcpServer, listHermesMcpServers, removeHermesMcpServer } from "./mcpList.js";
|
|
8
8
|
import { runHermesUpdate } from "./hermesUpdate.js";
|
|
9
9
|
import { executeFilesList, executeFilesRead, executeFilesWrite, executeFilesDelete, executeMemoriesList, } from "./hermesFileCommands.js";
|
|
10
|
+
import { renameHermesSession } from "./renameHermesSession.js";
|
|
10
11
|
import { executeShellExec, resetShellSession } from "./shellSession.js";
|
|
11
12
|
import { fetchHermesRuntimeVersion } from "./runtimeVersion.js";
|
|
12
13
|
import { fetchBridgeRuntimeVersion, fetchInstalledBridgeVersion } from "./bridgeVersion.js";
|
|
@@ -42,6 +43,7 @@ export const HERMES_COMMAND_NAMES = [
|
|
|
42
43
|
"sessions.messages.list",
|
|
43
44
|
"sessions.messages.countSent",
|
|
44
45
|
"sessions.titles.resolve",
|
|
46
|
+
"sessions.rename",
|
|
45
47
|
"sessions.usage.get",
|
|
46
48
|
"sessions.list",
|
|
47
49
|
"skills.list",
|
|
@@ -355,6 +357,11 @@ export async function executeHermesCommand(command, args, options = {}) {
|
|
|
355
357
|
const sessionIds = optionalStringArray(args, "sessionIds") ?? [];
|
|
356
358
|
return { titles: resolveSessionTitles(sessionIds) };
|
|
357
359
|
}
|
|
360
|
+
case "sessions.rename": {
|
|
361
|
+
const sessionId = requireString(args, "sessionId");
|
|
362
|
+
const title = requireString(args, "title");
|
|
363
|
+
return await renameHermesSession(sessionId, title);
|
|
364
|
+
}
|
|
358
365
|
case "sessions.usage.get": {
|
|
359
366
|
const sessionId = requireString(args, "sessionId");
|
|
360
367
|
return getSessionUsage(sessionId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hermesForwarder.d.ts","sourceRoot":"","sources":["../src/hermesForwarder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAyB,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"hermesForwarder.d.ts","sourceRoot":"","sources":["../src/hermesForwarder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAyB,eAAe,EAAE,MAAM,aAAa,CAAC;AAkBtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAkBF,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,sBAA2B,GAAG;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf,CAUA;AAgID,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,eAAe,EACrB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAkF9B;AA2BD,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,sBAA2B,IAC/D,SAAS,MAAM,EAAE,MAAM,eAAe,EAAE,OAAO,UAAU,KAAG,OAAO,CAAC,IAAI,CAAC,CAMxF"}
|
package/dist/hermesForwarder.js
CHANGED
|
@@ -3,7 +3,8 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { linkHermesMessageIds, uploadFileToConvex } from "./convexRelay.js";
|
|
5
5
|
import { DEFAULT_HERMES_API_URL } from "./constants.js";
|
|
6
|
-
import { getLatestTurnMessageIds, hermesStateDbExists } from "./hermesSessionDb.js";
|
|
6
|
+
import { getLatestTurnMessageIds, getSessionUsage, hermesStateDbExists } from "./hermesSessionDb.js";
|
|
7
|
+
import { buildTurnActivity, createTurnActivityAccumulator, parseSseDataLines, processSseEventBlock, } from "./turnActivity.js";
|
|
7
8
|
import { parseMediaReferences, resolveMediaReferenceBytes, stripMediaReferences, } from "./hermesFiles.js";
|
|
8
9
|
function readHermesApiKeyFromEnvFile() {
|
|
9
10
|
const envPath = join(homedir(), ".hermes", ".env");
|
|
@@ -49,70 +50,6 @@ async function ensureHermesReachable(apiUrl, apiKey) {
|
|
|
49
50
|
throw new Error(`Hermes is not reachable at ${healthUrl}. Ensure Hermes is installed and run \`hermes gateway\` on this machine. ${message}`);
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
function parseSseDataLines(buffer) {
|
|
53
|
-
const parts = buffer.split("\n\n");
|
|
54
|
-
const rest = parts.pop() ?? "";
|
|
55
|
-
return { events: parts, rest };
|
|
56
|
-
}
|
|
57
|
-
function extractDeltaFromChunk(payload) {
|
|
58
|
-
const choices = payload.choices;
|
|
59
|
-
if (!Array.isArray(choices) || choices.length === 0)
|
|
60
|
-
return null;
|
|
61
|
-
const choice = choices[0];
|
|
62
|
-
if (!choice || typeof choice !== "object")
|
|
63
|
-
return null;
|
|
64
|
-
const delta = choice.delta;
|
|
65
|
-
if (!delta || typeof delta !== "object")
|
|
66
|
-
return null;
|
|
67
|
-
const content = delta.content;
|
|
68
|
-
return typeof content === "string" ? content : null;
|
|
69
|
-
}
|
|
70
|
-
function processSseEventBlock(eventBlock, reply, counters, onTextDelta) {
|
|
71
|
-
let eventName = "message";
|
|
72
|
-
for (const line of eventBlock.split("\n")) {
|
|
73
|
-
if (line.startsWith("event:")) {
|
|
74
|
-
eventName = line.slice(6).trim();
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (!line.startsWith("data:"))
|
|
78
|
-
continue;
|
|
79
|
-
const data = line.slice(5).trim();
|
|
80
|
-
if (!data || data === "[DONE]")
|
|
81
|
-
continue;
|
|
82
|
-
if (eventName === "hermes.tool.progress") {
|
|
83
|
-
try {
|
|
84
|
-
const payload = JSON.parse(data);
|
|
85
|
-
const tool = typeof payload.tool === "string" ? payload.tool : undefined;
|
|
86
|
-
const label = typeof payload.label === "string" ? payload.label : undefined;
|
|
87
|
-
const emoji = typeof payload.emoji === "string" ? payload.emoji : undefined;
|
|
88
|
-
reply.activity({
|
|
89
|
-
activityType: "tool.started",
|
|
90
|
-
tool,
|
|
91
|
-
label,
|
|
92
|
-
emoji,
|
|
93
|
-
sequence: counters.activitySequence,
|
|
94
|
-
});
|
|
95
|
-
counters.activitySequence += 1;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
// Ignore malformed tool progress payloads.
|
|
99
|
-
}
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
const payload = JSON.parse(data);
|
|
104
|
-
const delta = extractDeltaFromChunk(payload);
|
|
105
|
-
if (delta && delta.trim()) {
|
|
106
|
-
onTextDelta(delta);
|
|
107
|
-
reply.delta(delta, counters.textSequence);
|
|
108
|
-
counters.textSequence += 1;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// Ignore malformed SSE chunks.
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
53
|
function buildHermesUserContent(text, attachments) {
|
|
117
54
|
const trimmed = text.trim();
|
|
118
55
|
const imageParts = (attachments ?? [])
|
|
@@ -233,6 +170,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
233
170
|
throw new Error(`Hermes request failed (${response.status}): ${text}`);
|
|
234
171
|
}
|
|
235
172
|
const resolvedSessionId = response.headers.get("X-Hermes-Session-Id")?.trim() || sessionId;
|
|
173
|
+
const modelFromHeader = response.headers.get("X-Hermes-Model")?.trim() || undefined;
|
|
236
174
|
if (!response.body) {
|
|
237
175
|
throw new Error("Hermes returned an empty streaming body");
|
|
238
176
|
}
|
|
@@ -240,6 +178,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
240
178
|
const decoder = new TextDecoder();
|
|
241
179
|
let buffer = "";
|
|
242
180
|
const counters = { textSequence: 0, activitySequence: 0 };
|
|
181
|
+
const accumulator = createTurnActivityAccumulator();
|
|
243
182
|
let fullText = "";
|
|
244
183
|
while (true) {
|
|
245
184
|
const { done, value } = await reader.read();
|
|
@@ -249,7 +188,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
249
188
|
const { events, rest } = parseSseDataLines(buffer);
|
|
250
189
|
buffer = rest;
|
|
251
190
|
for (const eventBlock of events) {
|
|
252
|
-
processSseEventBlock(eventBlock, reply, counters, (delta) => {
|
|
191
|
+
processSseEventBlock(eventBlock, reply, counters, accumulator, (delta) => {
|
|
253
192
|
fullText += delta;
|
|
254
193
|
});
|
|
255
194
|
}
|
|
@@ -263,10 +202,33 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
263
202
|
const attachments = mediaRefs.length > 0 ? await uploadMediaAttachments(meta.agentId, mediaRefs) : [];
|
|
264
203
|
const displayText = attachments.length > 0 ? stripMediaReferences(fullText, mediaRefs) : fullText.trim() || fullText;
|
|
265
204
|
const { assistantMessageId } = getLatestTurnMessageIds(resolvedSessionId);
|
|
266
|
-
|
|
205
|
+
const replyModel = resolveReplyModel(modelFromHeader, resolvedSessionId, model);
|
|
206
|
+
const turnActivity = buildTurnActivity(accumulator, replyModel);
|
|
207
|
+
reply.complete(displayText, counters.textSequence, attachments.length > 0 ? attachments : undefined, assistantMessageId !== undefined ? String(assistantMessageId) : undefined, turnActivity);
|
|
267
208
|
await linkTurnToConvex(meta, resolvedSessionId);
|
|
268
209
|
return { sessionId: resolvedSessionId };
|
|
269
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Resolves which model produced the reply, in priority order:
|
|
213
|
+
* 1. The `X-Hermes-Model` response header.
|
|
214
|
+
* 2. The session's recorded model in Hermes `state.db`.
|
|
215
|
+
* 3. The model from the outbound request (skipping the generic placeholder alias).
|
|
216
|
+
*/
|
|
217
|
+
function resolveReplyModel(modelFromHeader, resolvedSessionId, requestModel) {
|
|
218
|
+
if (modelFromHeader)
|
|
219
|
+
return modelFromHeader;
|
|
220
|
+
if (hermesStateDbExists()) {
|
|
221
|
+
try {
|
|
222
|
+
const usage = getSessionUsage(resolvedSessionId);
|
|
223
|
+
if (usage.model)
|
|
224
|
+
return usage.model;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Fall through to request model.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return requestModel && requestModel !== "hermes-agent" ? requestModel : undefined;
|
|
231
|
+
}
|
|
270
232
|
export function createHermesMessageHandler(options = {}) {
|
|
271
233
|
return async (content, meta, reply) => {
|
|
272
234
|
await forwardToHermes(content, meta, reply, {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renameHermesSession.d.ts","sourceRoot":"","sources":["../src/renameHermesSession.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,yBAAyB,GAAG;IACtC,EAAE,EAAE,IAAI,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAgBF,wBAAsB,mBAAmB,CACvC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,yBAAyB,CAAC,CAmBpC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const RENAME_TIMEOUT_MS = 30_000;
|
|
5
|
+
function execFailureMessage(error) {
|
|
6
|
+
if (error && typeof error === "object") {
|
|
7
|
+
const record = error;
|
|
8
|
+
const stderr = record.stderr?.trim();
|
|
9
|
+
if (stderr)
|
|
10
|
+
return stderr;
|
|
11
|
+
const stdout = record.stdout?.trim();
|
|
12
|
+
if (stdout)
|
|
13
|
+
return stdout;
|
|
14
|
+
if (typeof record.message === "string" && record.message.trim()) {
|
|
15
|
+
return record.message.trim();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return "Could not rename Hermes session.";
|
|
19
|
+
}
|
|
20
|
+
export async function renameHermesSession(sessionId, title) {
|
|
21
|
+
const trimmedId = sessionId.trim();
|
|
22
|
+
const trimmedTitle = title.trim().slice(0, 120);
|
|
23
|
+
if (!trimmedId) {
|
|
24
|
+
throw new Error('Missing or invalid "sessionId"');
|
|
25
|
+
}
|
|
26
|
+
if (!trimmedTitle) {
|
|
27
|
+
throw new Error('Missing or invalid "title"');
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await execFileAsync("hermes", ["sessions", "rename", trimmedId, trimmedTitle], {
|
|
31
|
+
timeout: RENAME_TIMEOUT_MS,
|
|
32
|
+
env: process.env,
|
|
33
|
+
});
|
|
34
|
+
return { ok: true, sessionId: trimmedId, title: trimmedTitle };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new Error(execFailureMessage(error));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renameHermesSession.test.d.ts","sourceRoot":"","sources":["../src/renameHermesSession.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { renameHermesSession } from "./renameHermesSession.js";
|
|
4
|
+
test("renameHermesSession rejects empty session id", async () => {
|
|
5
|
+
await assert.rejects(() => renameHermesSession(" ", "My session"), (error) => {
|
|
6
|
+
assert.match(String(error), /sessionId/i);
|
|
7
|
+
return true;
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
test("renameHermesSession rejects empty title", async () => {
|
|
11
|
+
await assert.rejects(() => renameHermesSession("20250305_091523_a1b2c3", " "), (error) => {
|
|
12
|
+
assert.match(String(error), /title/i);
|
|
13
|
+
return true;
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AgentReply, AgentTurnActivity } from "./client.js";
|
|
2
|
+
export type StreamCounters = {
|
|
3
|
+
textSequence: number;
|
|
4
|
+
activitySequence: number;
|
|
5
|
+
};
|
|
6
|
+
export type TurnActivityAccumulator = {
|
|
7
|
+
tools: Map<string, {
|
|
8
|
+
name: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
emoji?: string;
|
|
11
|
+
count: number;
|
|
12
|
+
source?: string;
|
|
13
|
+
}>;
|
|
14
|
+
skills: Map<string, {
|
|
15
|
+
name: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
}>;
|
|
18
|
+
memories: Map<string, {
|
|
19
|
+
name: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
}>;
|
|
22
|
+
mcps: Map<string, {
|
|
23
|
+
server: string;
|
|
24
|
+
tool?: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
count: number;
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
export declare function createTurnActivityAccumulator(): TurnActivityAccumulator;
|
|
30
|
+
export declare function buildTurnActivity(acc: TurnActivityAccumulator, model?: string): AgentTurnActivity;
|
|
31
|
+
export declare function parseSseDataLines(buffer: string): {
|
|
32
|
+
events: string[];
|
|
33
|
+
rest: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function processSseEventBlock(eventBlock: string, reply: AgentReply, counters: StreamCounters, accumulator: TurnActivityAccumulator, onTextDelta: (delta: string) => void): void;
|
|
36
|
+
//# sourceMappingURL=turnActivity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnActivity.d.ts","sourceRoot":"","sources":["../src/turnActivity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEjE,MAAM,MAAM,cAAc,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAgCF,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrG,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACrF,CAAC;AAEF,wBAAgB,6BAA6B,IAAI,uBAAuB,CAEvE;AAmCD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,uBAAuB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA6CjG;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAIpF;AAiBD,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,uBAAuB,EACpC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACnC,IAAI,CA4IN"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/** Caps how many distinct items per category are persisted on a message summary. */
|
|
2
|
+
const MAX_TURN_ACTIVITY_ITEMS = 20;
|
|
3
|
+
export function createTurnActivityAccumulator() {
|
|
4
|
+
return { tools: new Map(), skills: new Map(), memories: new Map(), mcps: new Map() };
|
|
5
|
+
}
|
|
6
|
+
function recordToolUse(acc, name, label, emoji, source) {
|
|
7
|
+
const existing = acc.tools.get(name);
|
|
8
|
+
if (existing) {
|
|
9
|
+
existing.count += 1;
|
|
10
|
+
if (!existing.label && label)
|
|
11
|
+
existing.label = label;
|
|
12
|
+
if (!existing.emoji && emoji)
|
|
13
|
+
existing.emoji = emoji;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
acc.tools.set(name, { name, label, emoji, count: 1, source });
|
|
17
|
+
}
|
|
18
|
+
function recordMcpUse(acc, server, tool, label) {
|
|
19
|
+
const key = `${server}\u0000${tool ?? ""}`;
|
|
20
|
+
const existing = acc.mcps.get(key);
|
|
21
|
+
if (existing) {
|
|
22
|
+
existing.count += 1;
|
|
23
|
+
if (!existing.label && label)
|
|
24
|
+
existing.label = label;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
acc.mcps.set(key, { server, tool, label, count: 1 });
|
|
28
|
+
}
|
|
29
|
+
export function buildTurnActivity(acc, model) {
|
|
30
|
+
const activity = {};
|
|
31
|
+
const tools = [...acc.tools.values()].slice(0, MAX_TURN_ACTIVITY_ITEMS);
|
|
32
|
+
if (tools.length > 0) {
|
|
33
|
+
activity.tools = tools.map((tool) => ({
|
|
34
|
+
name: tool.name,
|
|
35
|
+
...(tool.label ? { label: tool.label } : {}),
|
|
36
|
+
...(tool.emoji ? { emoji: tool.emoji } : {}),
|
|
37
|
+
...(tool.count > 1 ? { count: tool.count } : {}),
|
|
38
|
+
...(tool.source ? { source: tool.source } : {}),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
const skills = [...acc.skills.values()].slice(0, MAX_TURN_ACTIVITY_ITEMS);
|
|
42
|
+
if (skills.length > 0) {
|
|
43
|
+
activity.skills = skills.map((skill) => ({
|
|
44
|
+
name: skill.name,
|
|
45
|
+
...(skill.label ? { label: skill.label } : {}),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
const memories = [...acc.memories.values()].slice(0, MAX_TURN_ACTIVITY_ITEMS);
|
|
49
|
+
if (memories.length > 0) {
|
|
50
|
+
activity.memories = memories.map((memory) => ({
|
|
51
|
+
name: memory.name,
|
|
52
|
+
...(memory.label ? { label: memory.label } : {}),
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
const mcps = [...acc.mcps.values()].slice(0, MAX_TURN_ACTIVITY_ITEMS);
|
|
56
|
+
if (mcps.length > 0) {
|
|
57
|
+
activity.mcps = mcps.map((mcp) => ({
|
|
58
|
+
server: mcp.server,
|
|
59
|
+
...(mcp.tool ? { tool: mcp.tool } : {}),
|
|
60
|
+
...(mcp.label ? { label: mcp.label } : {}),
|
|
61
|
+
...(mcp.count > 1 ? { count: mcp.count } : {}),
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
if (model) {
|
|
65
|
+
activity.model = model;
|
|
66
|
+
}
|
|
67
|
+
return activity;
|
|
68
|
+
}
|
|
69
|
+
export function parseSseDataLines(buffer) {
|
|
70
|
+
const parts = buffer.split("\n\n");
|
|
71
|
+
const rest = parts.pop() ?? "";
|
|
72
|
+
return { events: parts, rest };
|
|
73
|
+
}
|
|
74
|
+
function extractDeltaFromChunk(payload) {
|
|
75
|
+
const choices = payload.choices;
|
|
76
|
+
if (!Array.isArray(choices) || choices.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
const choice = choices[0];
|
|
79
|
+
if (!choice || typeof choice !== "object")
|
|
80
|
+
return null;
|
|
81
|
+
const delta = choice.delta;
|
|
82
|
+
if (!delta || typeof delta !== "object")
|
|
83
|
+
return null;
|
|
84
|
+
const content = delta.content;
|
|
85
|
+
return typeof content === "string" ? content : null;
|
|
86
|
+
}
|
|
87
|
+
function asString(value) {
|
|
88
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
export function processSseEventBlock(eventBlock, reply, counters, accumulator, onTextDelta) {
|
|
91
|
+
let eventName = "message";
|
|
92
|
+
for (const line of eventBlock.split("\n")) {
|
|
93
|
+
if (line.startsWith("event:")) {
|
|
94
|
+
eventName = line.slice(6).trim();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!line.startsWith("data:"))
|
|
98
|
+
continue;
|
|
99
|
+
const data = line.slice(5).trim();
|
|
100
|
+
if (!data || data === "[DONE]")
|
|
101
|
+
continue;
|
|
102
|
+
if (eventName === "hermes.tool.progress") {
|
|
103
|
+
try {
|
|
104
|
+
const payload = JSON.parse(data);
|
|
105
|
+
const tool = asString(payload.tool);
|
|
106
|
+
const label = asString(payload.label);
|
|
107
|
+
const emoji = asString(payload.emoji);
|
|
108
|
+
const source = asString(payload.source);
|
|
109
|
+
const server = asString(payload.server);
|
|
110
|
+
// MCP tool calls may arrive as tool progress; bucket them as MCP usage.
|
|
111
|
+
if (source === "mcp" || server) {
|
|
112
|
+
const mcpServer = server ?? "mcp";
|
|
113
|
+
recordMcpUse(accumulator, mcpServer, tool, label);
|
|
114
|
+
reply.activity({
|
|
115
|
+
activityType: "tool.started",
|
|
116
|
+
kind: "mcp",
|
|
117
|
+
tool,
|
|
118
|
+
server: mcpServer,
|
|
119
|
+
source: "mcp",
|
|
120
|
+
label,
|
|
121
|
+
emoji,
|
|
122
|
+
sequence: counters.activitySequence,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (tool)
|
|
127
|
+
recordToolUse(accumulator, tool, label, emoji, source);
|
|
128
|
+
reply.activity({
|
|
129
|
+
activityType: "tool.started",
|
|
130
|
+
kind: "tool",
|
|
131
|
+
tool,
|
|
132
|
+
source,
|
|
133
|
+
label,
|
|
134
|
+
emoji,
|
|
135
|
+
sequence: counters.activitySequence,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
counters.activitySequence += 1;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Ignore malformed tool progress payloads.
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (eventName === "hermes.skill.progress") {
|
|
146
|
+
try {
|
|
147
|
+
const payload = JSON.parse(data);
|
|
148
|
+
const skill = asString(payload.skill);
|
|
149
|
+
const label = asString(payload.label);
|
|
150
|
+
const emoji = asString(payload.emoji);
|
|
151
|
+
if (skill && !accumulator.skills.has(skill)) {
|
|
152
|
+
accumulator.skills.set(skill, { name: skill, label });
|
|
153
|
+
}
|
|
154
|
+
reply.activity({
|
|
155
|
+
activityType: "tool.started",
|
|
156
|
+
kind: "skill",
|
|
157
|
+
skill,
|
|
158
|
+
label,
|
|
159
|
+
emoji,
|
|
160
|
+
sequence: counters.activitySequence,
|
|
161
|
+
});
|
|
162
|
+
counters.activitySequence += 1;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Ignore malformed skill progress payloads.
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (eventName === "hermes.memory.progress") {
|
|
170
|
+
try {
|
|
171
|
+
const payload = JSON.parse(data);
|
|
172
|
+
const memory = asString(payload.memory);
|
|
173
|
+
const label = asString(payload.label);
|
|
174
|
+
const emoji = asString(payload.emoji);
|
|
175
|
+
if (memory && !accumulator.memories.has(memory)) {
|
|
176
|
+
accumulator.memories.set(memory, { name: memory, label });
|
|
177
|
+
}
|
|
178
|
+
reply.activity({
|
|
179
|
+
activityType: "tool.started",
|
|
180
|
+
kind: "memory",
|
|
181
|
+
memory,
|
|
182
|
+
label,
|
|
183
|
+
emoji,
|
|
184
|
+
sequence: counters.activitySequence,
|
|
185
|
+
});
|
|
186
|
+
counters.activitySequence += 1;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Ignore malformed memory progress payloads.
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (eventName === "hermes.mcp.progress") {
|
|
194
|
+
try {
|
|
195
|
+
const payload = JSON.parse(data);
|
|
196
|
+
const server = asString(payload.server);
|
|
197
|
+
const tool = asString(payload.tool);
|
|
198
|
+
const label = asString(payload.label);
|
|
199
|
+
const emoji = asString(payload.emoji);
|
|
200
|
+
if (server) {
|
|
201
|
+
recordMcpUse(accumulator, server, tool, label);
|
|
202
|
+
reply.activity({
|
|
203
|
+
activityType: "tool.started",
|
|
204
|
+
kind: "mcp",
|
|
205
|
+
server,
|
|
206
|
+
tool,
|
|
207
|
+
source: "mcp",
|
|
208
|
+
label,
|
|
209
|
+
emoji,
|
|
210
|
+
sequence: counters.activitySequence,
|
|
211
|
+
});
|
|
212
|
+
counters.activitySequence += 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Ignore malformed MCP progress payloads.
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const payload = JSON.parse(data);
|
|
222
|
+
const delta = extractDeltaFromChunk(payload);
|
|
223
|
+
if (delta && delta.trim()) {
|
|
224
|
+
onTextDelta(delta);
|
|
225
|
+
reply.delta(delta, counters.textSequence);
|
|
226
|
+
counters.textSequence += 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Ignore malformed SSE chunks.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turnActivity.test.d.ts","sourceRoot":"","sources":["../src/turnActivity.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildTurnActivity, createTurnActivityAccumulator, processSseEventBlock, } from "./turnActivity.js";
|
|
4
|
+
function collectingReply() {
|
|
5
|
+
const activities = [];
|
|
6
|
+
const deltas = [];
|
|
7
|
+
const reply = {
|
|
8
|
+
delta: (text) => {
|
|
9
|
+
deltas.push(text);
|
|
10
|
+
},
|
|
11
|
+
activity: (activity) => {
|
|
12
|
+
activities.push(activity);
|
|
13
|
+
},
|
|
14
|
+
complete: () => { },
|
|
15
|
+
failed: () => { },
|
|
16
|
+
};
|
|
17
|
+
return { reply, activities, deltas };
|
|
18
|
+
}
|
|
19
|
+
function sseEvent(event, data) {
|
|
20
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}`;
|
|
21
|
+
}
|
|
22
|
+
test("accumulates and dedupes tool usage with counts", () => {
|
|
23
|
+
const { reply, activities } = collectingReply();
|
|
24
|
+
const accumulator = createTurnActivityAccumulator();
|
|
25
|
+
const counters = { textSequence: 0, activitySequence: 0 };
|
|
26
|
+
processSseEventBlock(sseEvent("hermes.tool.progress", { tool: "web_search", label: "Searching", emoji: "🔍" }), reply, counters, accumulator, () => { });
|
|
27
|
+
processSseEventBlock(sseEvent("hermes.tool.progress", { tool: "web_search" }), reply, counters, accumulator, () => { });
|
|
28
|
+
const activity = buildTurnActivity(accumulator);
|
|
29
|
+
assert.deepEqual(activity.tools, [
|
|
30
|
+
{ name: "web_search", label: "Searching", emoji: "🔍", count: 2 },
|
|
31
|
+
]);
|
|
32
|
+
assert.equal(activities.length, 2);
|
|
33
|
+
assert.equal(activities[0]?.kind, "tool");
|
|
34
|
+
});
|
|
35
|
+
test("buckets MCP tool progress into mcps, not tools", () => {
|
|
36
|
+
const { reply } = collectingReply();
|
|
37
|
+
const accumulator = createTurnActivityAccumulator();
|
|
38
|
+
const counters = { textSequence: 0, activitySequence: 0 };
|
|
39
|
+
processSseEventBlock(sseEvent("hermes.tool.progress", { tool: "search", source: "mcp", server: "context7" }), reply, counters, accumulator, () => { });
|
|
40
|
+
processSseEventBlock(sseEvent("hermes.mcp.progress", { server: "context7", tool: "search" }), reply, counters, accumulator, () => { });
|
|
41
|
+
const activity = buildTurnActivity(accumulator);
|
|
42
|
+
assert.equal(activity.tools, undefined);
|
|
43
|
+
assert.deepEqual(activity.mcps, [{ server: "context7", tool: "search", count: 2 }]);
|
|
44
|
+
});
|
|
45
|
+
test("collects skills and memories uniquely", () => {
|
|
46
|
+
const { reply } = collectingReply();
|
|
47
|
+
const accumulator = createTurnActivityAccumulator();
|
|
48
|
+
const counters = { textSequence: 0, activitySequence: 0 };
|
|
49
|
+
processSseEventBlock(sseEvent("hermes.skill.progress", { skill: "frontend-design", label: "Frontend design" }), reply, counters, accumulator, () => { });
|
|
50
|
+
processSseEventBlock(sseEvent("hermes.skill.progress", { skill: "frontend-design" }), reply, counters, accumulator, () => { });
|
|
51
|
+
processSseEventBlock(sseEvent("hermes.memory.progress", { memory: "USER.md" }), reply, counters, accumulator, () => { });
|
|
52
|
+
const activity = buildTurnActivity(accumulator, "anthropic/claude-sonnet-4");
|
|
53
|
+
assert.deepEqual(activity.skills, [{ name: "frontend-design", label: "Frontend design" }]);
|
|
54
|
+
assert.deepEqual(activity.memories, [{ name: "USER.md" }]);
|
|
55
|
+
assert.equal(activity.model, "anthropic/claude-sonnet-4");
|
|
56
|
+
});
|
|
57
|
+
test("text deltas are forwarded and not treated as activity", () => {
|
|
58
|
+
const { reply, deltas, activities } = collectingReply();
|
|
59
|
+
const accumulator = createTurnActivityAccumulator();
|
|
60
|
+
const counters = { textSequence: 0, activitySequence: 0 };
|
|
61
|
+
let captured = "";
|
|
62
|
+
processSseEventBlock(`data: ${JSON.stringify({ choices: [{ delta: { content: "Hello" } }] })}`, reply, counters, accumulator, (delta) => {
|
|
63
|
+
captured += delta;
|
|
64
|
+
});
|
|
65
|
+
assert.equal(captured, "Hello");
|
|
66
|
+
assert.deepEqual(deltas, ["Hello"]);
|
|
67
|
+
assert.equal(activities.length, 0);
|
|
68
|
+
assert.equal(buildTurnActivity(accumulator).model, undefined);
|
|
69
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saleso.innovations/bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
4
4
|
"description": "Connect your Hermes agent to the Cleos iOS app via pairing code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"lint": "eslint . --max-warnings 0",
|
|
37
37
|
"check-types": "tsc --noEmit",
|
|
38
38
|
"prepublishOnly": "npm run build",
|
|
39
|
-
"test": "node --import tsx --test src/hermesFiles.test.ts src/hermesSessionDb.test.ts src/shellSession.test.ts src/bridgeVersion.test.ts"
|
|
39
|
+
"test": "node --import tsx --test src/hermesFiles.test.ts src/hermesSessionDb.test.ts src/renameHermesSession.test.ts src/shellSession.test.ts src/bridgeVersion.test.ts src/turnActivity.test.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"better-sqlite3": "^11.10.0",
|