@saleso.innovations/bridge 0.1.39 → 0.1.41
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 +37 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +22 -4
- package/dist/hermesForwarder.d.ts.map +1 -1
- package/dist/hermesForwarder.js +36 -67
- package/dist/skillLearnedDetector.d.ts +20 -0
- package/dist/skillLearnedDetector.d.ts.map +1 -0
- package/dist/skillLearnedDetector.js +53 -0
- package/dist/skillLearnedDetector.test.d.ts +2 -0
- package/dist/skillLearnedDetector.test.d.ts.map +1 -0
- package/dist/skillLearnedDetector.test.js +81 -0
- package/dist/skillsList.d.ts +7 -0
- package/dist/skillsList.d.ts.map +1 -1
- package/dist/skillsList.js +7 -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,49 @@ 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
|
+
skillsLearned?: Array<{
|
|
52
|
+
name: string;
|
|
53
|
+
label?: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
}>;
|
|
56
|
+
memories?: Array<{
|
|
57
|
+
name: string;
|
|
58
|
+
label?: string;
|
|
59
|
+
}>;
|
|
60
|
+
mcps?: Array<{
|
|
61
|
+
server: string;
|
|
62
|
+
tool?: string;
|
|
63
|
+
label?: string;
|
|
64
|
+
count?: number;
|
|
65
|
+
}>;
|
|
66
|
+
model?: string;
|
|
67
|
+
};
|
|
32
68
|
export type AgentFailedPayload = {
|
|
33
69
|
error: string;
|
|
34
70
|
code?: string;
|
|
@@ -37,7 +73,7 @@ export type AgentFailedPayload = {
|
|
|
37
73
|
export type AgentReply = {
|
|
38
74
|
delta: (text: string, sequence: number) => void;
|
|
39
75
|
activity: (activity: AgentActivityPayload) => void;
|
|
40
|
-
complete: (text: string, sequence: number, attachments?: UserMessageAttachment[], hermesAssistantMessageId?: string) => void;
|
|
76
|
+
complete: (text: string, sequence: number, attachments?: UserMessageAttachment[], hermesAssistantMessageId?: string, turnActivity?: AgentTurnActivity) => void;
|
|
41
77
|
failed: (failure: AgentFailedPayload) => void;
|
|
42
78
|
};
|
|
43
79
|
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,14 @@ 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.skillsLearned?.length ?? 0) > 0 ||
|
|
16
|
+
(activity.memories?.length ?? 0) > 0 ||
|
|
17
|
+
(activity.mcps?.length ?? 0) > 0 ||
|
|
18
|
+
typeof activity.model === "string");
|
|
19
|
+
}
|
|
12
20
|
function parseUserMessageAttachments(raw) {
|
|
13
21
|
if (!Array.isArray(raw))
|
|
14
22
|
return undefined;
|
|
@@ -71,13 +79,25 @@ function createReplySender(ws, agentId, conversationId, messageId) {
|
|
|
71
79
|
conversationId,
|
|
72
80
|
messageId,
|
|
73
81
|
activityType: activity.activityType,
|
|
82
|
+
kind: activity.kind,
|
|
74
83
|
tool: activity.tool,
|
|
84
|
+
skill: activity.skill,
|
|
85
|
+
memory: activity.memory,
|
|
86
|
+
server: activity.server,
|
|
87
|
+
source: activity.source,
|
|
75
88
|
label: activity.label,
|
|
76
89
|
emoji: activity.emoji,
|
|
77
90
|
sequence: activity.sequence,
|
|
78
91
|
}));
|
|
79
92
|
},
|
|
80
|
-
complete(text, sequence, attachments, hermesAssistantMessageId) {
|
|
93
|
+
complete(text, sequence, attachments, hermesAssistantMessageId, turnActivity) {
|
|
94
|
+
const metadata = {};
|
|
95
|
+
if (hermesAssistantMessageId) {
|
|
96
|
+
metadata.hermes = { assistantMessageId: hermesAssistantMessageId };
|
|
97
|
+
}
|
|
98
|
+
if (turnActivity && hasTurnActivity(turnActivity)) {
|
|
99
|
+
metadata.turnActivity = turnActivity;
|
|
100
|
+
}
|
|
81
101
|
ws.send(JSON.stringify({
|
|
82
102
|
type: "agent.message",
|
|
83
103
|
agentId,
|
|
@@ -87,9 +107,7 @@ function createReplySender(ws, agentId, conversationId, messageId) {
|
|
|
87
107
|
sequence,
|
|
88
108
|
final: true,
|
|
89
109
|
...(attachments && attachments.length > 0 ? { attachments } : {}),
|
|
90
|
-
...(
|
|
91
|
-
? { metadata: { hermes: { assistantMessageId: hermesAssistantMessageId } } }
|
|
92
|
-
: {}),
|
|
110
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
93
111
|
}));
|
|
94
112
|
},
|
|
95
113
|
failed(failure) {
|
|
@@ -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;AAmBtF,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,CAyF9B;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,9 @@ 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 { diffSkillsLearned, snapshotSkills } from "./skillLearnedDetector.js";
|
|
8
|
+
import { buildTurnActivity, createTurnActivityAccumulator, parseSseDataLines, processSseEventBlock, } from "./turnActivity.js";
|
|
7
9
|
import { parseMediaReferences, resolveMediaReferenceBytes, stripMediaReferences, } from "./hermesFiles.js";
|
|
8
10
|
function readHermesApiKeyFromEnvFile() {
|
|
9
11
|
const envPath = join(homedir(), ".hermes", ".env");
|
|
@@ -49,70 +51,6 @@ async function ensureHermesReachable(apiUrl, apiKey) {
|
|
|
49
51
|
throw new Error(`Hermes is not reachable at ${healthUrl}. Ensure Hermes is installed and run \`hermes gateway\` on this machine. ${message}`);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
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
54
|
function buildHermesUserContent(text, attachments) {
|
|
117
55
|
const trimmed = text.trim();
|
|
118
56
|
const imageParts = (attachments ?? [])
|
|
@@ -223,6 +161,8 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
223
161
|
messages: [{ role: "user", content: userContent }],
|
|
224
162
|
user: sessionId,
|
|
225
163
|
};
|
|
164
|
+
const turnStartedAt = Date.now();
|
|
165
|
+
const skillsBefore = snapshotSkills();
|
|
226
166
|
const response = await fetch(apiUrl, {
|
|
227
167
|
method: "POST",
|
|
228
168
|
headers,
|
|
@@ -233,6 +173,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
233
173
|
throw new Error(`Hermes request failed (${response.status}): ${text}`);
|
|
234
174
|
}
|
|
235
175
|
const resolvedSessionId = response.headers.get("X-Hermes-Session-Id")?.trim() || sessionId;
|
|
176
|
+
const modelFromHeader = response.headers.get("X-Hermes-Model")?.trim() || undefined;
|
|
236
177
|
if (!response.body) {
|
|
237
178
|
throw new Error("Hermes returned an empty streaming body");
|
|
238
179
|
}
|
|
@@ -240,6 +181,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
240
181
|
const decoder = new TextDecoder();
|
|
241
182
|
let buffer = "";
|
|
242
183
|
const counters = { textSequence: 0, activitySequence: 0 };
|
|
184
|
+
const accumulator = createTurnActivityAccumulator();
|
|
243
185
|
let fullText = "";
|
|
244
186
|
while (true) {
|
|
245
187
|
const { done, value } = await reader.read();
|
|
@@ -249,7 +191,7 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
249
191
|
const { events, rest } = parseSseDataLines(buffer);
|
|
250
192
|
buffer = rest;
|
|
251
193
|
for (const eventBlock of events) {
|
|
252
|
-
processSseEventBlock(eventBlock, reply, counters, (delta) => {
|
|
194
|
+
processSseEventBlock(eventBlock, reply, counters, accumulator, (delta) => {
|
|
253
195
|
fullText += delta;
|
|
254
196
|
});
|
|
255
197
|
}
|
|
@@ -263,10 +205,37 @@ export async function forwardToHermes(content, meta, reply, options = {}) {
|
|
|
263
205
|
const attachments = mediaRefs.length > 0 ? await uploadMediaAttachments(meta.agentId, mediaRefs) : [];
|
|
264
206
|
const displayText = attachments.length > 0 ? stripMediaReferences(fullText, mediaRefs) : fullText.trim() || fullText;
|
|
265
207
|
const { assistantMessageId } = getLatestTurnMessageIds(resolvedSessionId);
|
|
266
|
-
|
|
208
|
+
const replyModel = resolveReplyModel(modelFromHeader, resolvedSessionId, model);
|
|
209
|
+
const turnActivity = buildTurnActivity(accumulator, replyModel);
|
|
210
|
+
const skillsLearned = diffSkillsLearned(skillsBefore, snapshotSkills(), { turnStartedAt });
|
|
211
|
+
if (skillsLearned.length > 0) {
|
|
212
|
+
turnActivity.skillsLearned = skillsLearned;
|
|
213
|
+
}
|
|
214
|
+
reply.complete(displayText, counters.textSequence, attachments.length > 0 ? attachments : undefined, assistantMessageId !== undefined ? String(assistantMessageId) : undefined, turnActivity);
|
|
267
215
|
await linkTurnToConvex(meta, resolvedSessionId);
|
|
268
216
|
return { sessionId: resolvedSessionId };
|
|
269
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Resolves which model produced the reply, in priority order:
|
|
220
|
+
* 1. The `X-Hermes-Model` response header.
|
|
221
|
+
* 2. The session's recorded model in Hermes `state.db`.
|
|
222
|
+
* 3. The model from the outbound request (skipping the generic placeholder alias).
|
|
223
|
+
*/
|
|
224
|
+
function resolveReplyModel(modelFromHeader, resolvedSessionId, requestModel) {
|
|
225
|
+
if (modelFromHeader)
|
|
226
|
+
return modelFromHeader;
|
|
227
|
+
if (hermesStateDbExists()) {
|
|
228
|
+
try {
|
|
229
|
+
const usage = getSessionUsage(resolvedSessionId);
|
|
230
|
+
if (usage.model)
|
|
231
|
+
return usage.model;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// Fall through to request model.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return requestModel && requestModel !== "hermes-agent" ? requestModel : undefined;
|
|
238
|
+
}
|
|
270
239
|
export function createHermesMessageHandler(options = {}) {
|
|
271
240
|
return async (content, meta, reply) => {
|
|
272
241
|
await forwardToHermes(content, meta, reply, {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentTurnActivity } from "./client.js";
|
|
2
|
+
import { type HermesSkillEntry } from "./skillsList.js";
|
|
3
|
+
export type SkillSnapshotEntry = HermesSkillEntry & {
|
|
4
|
+
mtimeMs?: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Captures the set of skills currently on disk, keyed by relative path, with each
|
|
8
|
+
* file's mtime so a later diff can ignore writes that predate the turn.
|
|
9
|
+
*/
|
|
10
|
+
export declare function snapshotSkills(): Map<string, SkillSnapshotEntry>;
|
|
11
|
+
/**
|
|
12
|
+
* Returns skills present in `after` but not in `before` — i.e. newly written during the
|
|
13
|
+
* turn. When `turnStartedAt` is provided, entries whose file mtime predates the turn
|
|
14
|
+
* start (beyond a small grace window) are excluded to guard against concurrent
|
|
15
|
+
* background writes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function diffSkillsLearned(before: Map<string, SkillSnapshotEntry>, after: Map<string, SkillSnapshotEntry>, options?: {
|
|
18
|
+
turnStartedAt?: number;
|
|
19
|
+
}): NonNullable<AgentTurnActivity["skillsLearned"]>;
|
|
20
|
+
//# sourceMappingURL=skillLearnedDetector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillLearnedDetector.d.ts","sourceRoot":"","sources":["../src/skillLearnedDetector.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAElF,MAAM,MAAM,kBAAkB,GAAG,gBAAgB,GAAG;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,cAAc,IAAI,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAchE;AAKD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACvC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EACtC,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GACvC,WAAW,CAAC,iBAAiB,CAAC,eAAe,CAAC,CAAC,CAuBjD"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveHermesHome } from "./hermesFiles.js";
|
|
4
|
+
import { listSkillsFromFilesystem } from "./skillsList.js";
|
|
5
|
+
/**
|
|
6
|
+
* Captures the set of skills currently on disk, keyed by relative path, with each
|
|
7
|
+
* file's mtime so a later diff can ignore writes that predate the turn.
|
|
8
|
+
*/
|
|
9
|
+
export function snapshotSkills() {
|
|
10
|
+
const home = resolveHermesHome();
|
|
11
|
+
const snapshot = new Map();
|
|
12
|
+
for (const skill of listSkillsFromFilesystem().skills) {
|
|
13
|
+
if (!skill.relativePath)
|
|
14
|
+
continue;
|
|
15
|
+
let mtimeMs;
|
|
16
|
+
try {
|
|
17
|
+
mtimeMs = statSync(join(home, skill.relativePath)).mtimeMs;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// File may be unreadable; record without mtime.
|
|
21
|
+
}
|
|
22
|
+
snapshot.set(skill.relativePath, { ...skill, mtimeMs });
|
|
23
|
+
}
|
|
24
|
+
return snapshot;
|
|
25
|
+
}
|
|
26
|
+
/** Tolerance for clock skew between the turn start and a freshly written skill file. */
|
|
27
|
+
const MTIME_GRACE_MS = 5_000;
|
|
28
|
+
/**
|
|
29
|
+
* Returns skills present in `after` but not in `before` — i.e. newly written during the
|
|
30
|
+
* turn. When `turnStartedAt` is provided, entries whose file mtime predates the turn
|
|
31
|
+
* start (beyond a small grace window) are excluded to guard against concurrent
|
|
32
|
+
* background writes.
|
|
33
|
+
*/
|
|
34
|
+
export function diffSkillsLearned(before, after, options = {}) {
|
|
35
|
+
const learned = [];
|
|
36
|
+
const threshold = typeof options.turnStartedAt === "number" ? options.turnStartedAt - MTIME_GRACE_MS : undefined;
|
|
37
|
+
for (const [relativePath, entry] of after) {
|
|
38
|
+
if (before.has(relativePath))
|
|
39
|
+
continue;
|
|
40
|
+
if (threshold !== undefined &&
|
|
41
|
+
typeof entry.mtimeMs === "number" &&
|
|
42
|
+
entry.mtimeMs < threshold) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
learned.push({
|
|
46
|
+
name: entry.name,
|
|
47
|
+
...(entry.category ? { label: entry.category } : {}),
|
|
48
|
+
...(entry.description ? { description: entry.description } : {}),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
learned.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
52
|
+
return learned;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skillLearnedDetector.test.d.ts","sourceRoot":"","sources":["../src/skillLearnedDetector.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { diffSkillsLearned } from "./skillLearnedDetector.js";
|
|
4
|
+
function snapshot(entries) {
|
|
5
|
+
const map = new Map();
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
if (entry.relativePath)
|
|
8
|
+
map.set(entry.relativePath, entry);
|
|
9
|
+
}
|
|
10
|
+
return map;
|
|
11
|
+
}
|
|
12
|
+
test("returns empty when nothing changed", () => {
|
|
13
|
+
const before = snapshot([
|
|
14
|
+
{ name: "alpha", description: "", relativePath: "skills/alpha/SKILL.md" },
|
|
15
|
+
]);
|
|
16
|
+
const after = snapshot([
|
|
17
|
+
{ name: "alpha", description: "", relativePath: "skills/alpha/SKILL.md" },
|
|
18
|
+
]);
|
|
19
|
+
assert.deepEqual(diffSkillsLearned(before, after), []);
|
|
20
|
+
});
|
|
21
|
+
test("detects a single new skill with label and description", () => {
|
|
22
|
+
const before = snapshot([]);
|
|
23
|
+
const after = snapshot([
|
|
24
|
+
{
|
|
25
|
+
name: "crypto-technical-analysis",
|
|
26
|
+
description: "Analyze crypto charts",
|
|
27
|
+
category: "trading",
|
|
28
|
+
relativePath: "skills/trading/crypto-technical-analysis/SKILL.md",
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
assert.deepEqual(diffSkillsLearned(before, after), [
|
|
32
|
+
{
|
|
33
|
+
name: "crypto-technical-analysis",
|
|
34
|
+
label: "trading",
|
|
35
|
+
description: "Analyze crypto charts",
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
test("detects multiple new skills sorted by name", () => {
|
|
40
|
+
const before = snapshot([
|
|
41
|
+
{ name: "alpha", description: "", relativePath: "skills/alpha/SKILL.md" },
|
|
42
|
+
]);
|
|
43
|
+
const after = snapshot([
|
|
44
|
+
{ name: "alpha", description: "", relativePath: "skills/alpha/SKILL.md" },
|
|
45
|
+
{ name: "zeta", description: "", relativePath: "skills/zeta/SKILL.md" },
|
|
46
|
+
{ name: "beta", description: "", relativePath: "skills/beta/SKILL.md" },
|
|
47
|
+
]);
|
|
48
|
+
assert.deepEqual(diffSkillsLearned(before, after).map((skill) => skill.name), ["beta", "zeta"]);
|
|
49
|
+
});
|
|
50
|
+
test("excludes new entries whose mtime predates the turn start", () => {
|
|
51
|
+
const turnStartedAt = 1_000_000;
|
|
52
|
+
const before = snapshot([]);
|
|
53
|
+
const after = snapshot([
|
|
54
|
+
{
|
|
55
|
+
name: "stale",
|
|
56
|
+
description: "",
|
|
57
|
+
relativePath: "skills/stale/SKILL.md",
|
|
58
|
+
mtimeMs: turnStartedAt - 60_000,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "fresh",
|
|
62
|
+
description: "",
|
|
63
|
+
relativePath: "skills/fresh/SKILL.md",
|
|
64
|
+
mtimeMs: turnStartedAt + 1_000,
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
assert.deepEqual(diffSkillsLearned(before, after, { turnStartedAt }).map((skill) => skill.name), ["fresh"]);
|
|
68
|
+
});
|
|
69
|
+
test("keeps new entries within the mtime grace window", () => {
|
|
70
|
+
const turnStartedAt = 1_000_000;
|
|
71
|
+
const before = snapshot([]);
|
|
72
|
+
const after = snapshot([
|
|
73
|
+
{
|
|
74
|
+
name: "borderline",
|
|
75
|
+
description: "",
|
|
76
|
+
relativePath: "skills/borderline/SKILL.md",
|
|
77
|
+
mtimeMs: turnStartedAt - 2_000,
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
assert.deepEqual(diffSkillsLearned(before, after, { turnStartedAt }).map((skill) => skill.name), ["borderline"]);
|
|
81
|
+
});
|
package/dist/skillsList.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ export type HermesSkillEntry = {
|
|
|
4
4
|
category?: string;
|
|
5
5
|
relativePath?: string;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* Synchronously reads the Hermes skills directory from disk. Used for fast turn-boundary
|
|
9
|
+
* snapshots without shelling out to the `hermes` CLI.
|
|
10
|
+
*/
|
|
11
|
+
export declare function listSkillsFromFilesystem(): {
|
|
12
|
+
skills: HermesSkillEntry[];
|
|
13
|
+
};
|
|
7
14
|
export declare function listHermesSkills(): Promise<{
|
|
8
15
|
skills: HermesSkillEntry[];
|
|
9
16
|
}>;
|
package/dist/skillsList.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skillsList.d.ts","sourceRoot":"","sources":["../src/skillsList.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;
|
|
1
|
+
{"version":3,"file":"skillsList.d.ts","sourceRoot":"","sources":["../src/skillsList.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AA6GF;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI;IAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAEzE;AA4HD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC;IAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA;CAAE,CAAC,CAOhF"}
|
package/dist/skillsList.js
CHANGED
|
@@ -105,6 +105,13 @@ function toHermesRelativeSkillPath(skillsRootDir, skillFilePath) {
|
|
|
105
105
|
const fromSkillsDir = relative(skillsRootDir, skillFilePath).replace(/\\/g, "/");
|
|
106
106
|
return `${SKILLS_DIR}/${fromSkillsDir}`;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Synchronously reads the Hermes skills directory from disk. Used for fast turn-boundary
|
|
110
|
+
* snapshots without shelling out to the `hermes` CLI.
|
|
111
|
+
*/
|
|
112
|
+
export function listSkillsFromFilesystem() {
|
|
113
|
+
return readSkillsFromDirectory();
|
|
114
|
+
}
|
|
108
115
|
function readSkillsFromDirectory() {
|
|
109
116
|
const root = skillsRoot();
|
|
110
117
|
if (!existsSync(root)) {
|
|
@@ -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.41",
|
|
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/renameHermesSession.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 src/skillLearnedDetector.test.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"better-sqlite3": "^11.10.0",
|