@nopeek/agent-bridge 0.7.15 → 0.7.17
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/dist/tool-progress.d.ts +15 -1
- package/dist/tool-progress.js +127 -43
- package/package.json +1 -1
package/dist/tool-progress.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export type ToolProgressEvent = {
|
|
|
7
7
|
/** One Telegram-style line. Running events only; completed is silent. */
|
|
8
8
|
export declare function formatToolLine(ev: ToolProgressEvent): string | null;
|
|
9
9
|
/** Hint so Hermes ends a tool-using turn with a recap, like Telegram. */
|
|
10
|
-
export declare const RECAP_HINT = "When this turn used tools
|
|
10
|
+
export declare const RECAP_HINT = "When this turn used tools, your LAST message must be a short phone briefing: (1) what was wrong, (2) what you did, (3) the solution / current state. Never end on a tool list, a diff, code, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
|
|
11
|
+
/** Posted when tools ran but the model never wrote a human recap. */
|
|
12
|
+
export declare const MISSING_BRIEFING = "Done. I finished that work. Ask if you want the recap: what was wrong, what I did, and where things stand.";
|
|
11
13
|
export interface StreamHandle {
|
|
12
14
|
append(chunk: string): void;
|
|
13
15
|
replace(full: string): void;
|
|
@@ -27,12 +29,19 @@ export type TurnPublisherOpts = {
|
|
|
27
29
|
maxProgressLines?: number;
|
|
28
30
|
/** Split commentary/recap into a new bubble after this many chars. */
|
|
29
31
|
maxBubbleChars?: number;
|
|
32
|
+
/** If nothing arrives for this long, the next chunk is a new message. 0 = off. */
|
|
33
|
+
bubbleGapMs?: number;
|
|
30
34
|
};
|
|
31
35
|
export type TurnFinishKind = "streamed" | "sent" | "empty";
|
|
32
36
|
/** Text in `full` that has not already been posted as commentary. */
|
|
33
37
|
export declare function unpublishedTail(full: string, published: string): string;
|
|
34
38
|
/** Unified diffs / `| review` dumps should never land in the chat. */
|
|
39
|
+
export declare function isDumpLine(line: string): boolean;
|
|
40
|
+
/** True when the text has a user-readable recap, not just tools/dumps. */
|
|
41
|
+
export declare function isHumanBriefing(text: string): boolean;
|
|
35
42
|
export declare function isToolDump(text: string): boolean;
|
|
43
|
+
/** Drop dump lines from a live chunk without swallowing normal punctuation. */
|
|
44
|
+
export declare function stripDumps(text: string): string;
|
|
36
45
|
/** Split a reply into short chat bubbles. Drops tool-dump paragraphs. */
|
|
37
46
|
export declare function splitBubbles(text: string, max?: number): string[];
|
|
38
47
|
/**
|
|
@@ -53,16 +62,21 @@ export declare class TurnPublisher {
|
|
|
53
62
|
private published;
|
|
54
63
|
private toolsUsed;
|
|
55
64
|
private startTimer;
|
|
65
|
+
private gapTimer;
|
|
56
66
|
private seen;
|
|
57
67
|
private readonly startDelayMs;
|
|
58
68
|
private readonly maxProgressLines;
|
|
59
69
|
private readonly maxBubbleChars;
|
|
70
|
+
private readonly bubbleGapMs;
|
|
60
71
|
constructor(sink: TurnSink, opts: TurnPublisherOpts);
|
|
61
72
|
onChunk(delta: string): void;
|
|
62
73
|
onTool(ev: ToolProgressEvent): void;
|
|
63
74
|
finish(reply: string): Promise<TurnFinishKind>;
|
|
64
75
|
fail(text: string): Promise<void>;
|
|
65
76
|
private cancelStart;
|
|
77
|
+
private cancelGap;
|
|
78
|
+
/** After `bubbleGapMs` of silence, seal the current bubble. Next text is new. */
|
|
79
|
+
private armGap;
|
|
66
80
|
private enqueue;
|
|
67
81
|
/** Open or append the current commentary/answer stream with this chunk only. */
|
|
68
82
|
private appendCommentary;
|
package/dist/tool-progress.js
CHANGED
|
@@ -49,7 +49,9 @@ function tidyLabel(raw) {
|
|
|
49
49
|
return s.length > 80 ? `${s.slice(0, 77)}...` : s;
|
|
50
50
|
}
|
|
51
51
|
/** Hint so Hermes ends a tool-using turn with a recap, like Telegram. */
|
|
52
|
-
export const RECAP_HINT = "When this turn used tools
|
|
52
|
+
export const RECAP_HINT = "When this turn used tools, your LAST message must be a short phone briefing: (1) what was wrong, (2) what you did, (3) the solution / current state. Never end on a tool list, a diff, code, or \"tasks completed\". Skip the briefing only for simple chat with no tools. No em dashes.";
|
|
53
|
+
/** Posted when tools ran but the model never wrote a human recap. */
|
|
54
|
+
export const MISSING_BRIEFING = "Done. I finished that work. Ask if you want the recap: what was wrong, what I did, and where things stand.";
|
|
53
55
|
/** Text in `full` that has not already been posted as commentary. */
|
|
54
56
|
export function unpublishedTail(full, published) {
|
|
55
57
|
const f = full.trim();
|
|
@@ -77,26 +79,82 @@ export function unpublishedTail(full, published) {
|
|
|
77
79
|
}
|
|
78
80
|
const DEFAULT_MAX_BUBBLE = 480;
|
|
79
81
|
/** Unified diffs / `| review` dumps should never land in the chat. */
|
|
82
|
+
export function isDumpLine(line) {
|
|
83
|
+
const l = line.trim();
|
|
84
|
+
if (!l)
|
|
85
|
+
return false;
|
|
86
|
+
if (/^review diff\b/i.test(l))
|
|
87
|
+
return true;
|
|
88
|
+
if (/^\|\s*(review|read|search|terminal|patch|write)\b/i.test(l))
|
|
89
|
+
return true;
|
|
90
|
+
if (/^@@\s+-/.test(l))
|
|
91
|
+
return true;
|
|
92
|
+
if (/^diff --git /.test(l))
|
|
93
|
+
return true;
|
|
94
|
+
if (/^\*\*\*\s+(Begin|Update|Add|Delete|End)\b/.test(l))
|
|
95
|
+
return true;
|
|
96
|
+
if (/^[ab]:?\/{1,2}Volumes\//.test(l))
|
|
97
|
+
return true;
|
|
98
|
+
if (/^index [0-9a-f]+\.\.[0-9a-f]+/.test(l))
|
|
99
|
+
return true;
|
|
100
|
+
// Unified-diff / review lines: "+ const x =" not "+1 country".
|
|
101
|
+
if (/^[+-]\s{2,}\S/.test(l))
|
|
102
|
+
return true;
|
|
103
|
+
if (/^[+-]\s*(import |from |export |const |let |function |class |def |if cmd\b|return )/.test(l))
|
|
104
|
+
return true;
|
|
105
|
+
if (/^[+-]\s*["'`{]/.test(l))
|
|
106
|
+
return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function isToolProgressLine(line) {
|
|
110
|
+
const l = line.trim();
|
|
111
|
+
if (!l)
|
|
112
|
+
return false;
|
|
113
|
+
return /^.+ \S+: ".+"$/.test(l) || /^.+ \S+\.\.\.$/.test(l);
|
|
114
|
+
}
|
|
115
|
+
/** True when the text has a user-readable recap, not just tools/dumps. */
|
|
116
|
+
export function isHumanBriefing(text) {
|
|
117
|
+
const prose = text
|
|
118
|
+
.split("\n")
|
|
119
|
+
.filter((l) => {
|
|
120
|
+
const t = l.trim();
|
|
121
|
+
return Boolean(t) && !isDumpLine(t) && !isToolProgressLine(t);
|
|
122
|
+
})
|
|
123
|
+
.join("\n")
|
|
124
|
+
.trim();
|
|
125
|
+
if (!prose || isToolDump(prose))
|
|
126
|
+
return false;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
80
129
|
export function isToolDump(text) {
|
|
81
130
|
const t = text.trim();
|
|
82
131
|
if (!t)
|
|
83
132
|
return false;
|
|
84
|
-
if (
|
|
85
|
-
return true;
|
|
86
|
-
if (/^@@\s+-\d+/.test(t))
|
|
87
|
-
return true;
|
|
88
|
-
if (/^\*\*\*\s+(Begin|Update|Add|Delete) Patch/m.test(t))
|
|
133
|
+
if (isDumpLine(t))
|
|
89
134
|
return true;
|
|
90
135
|
if (/^diff --git /m.test(t) && /^@@ /m.test(t))
|
|
91
136
|
return true;
|
|
92
137
|
const lines = t.split("\n");
|
|
93
138
|
if (lines.length >= 6) {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
139
|
+
const dumpy = lines.filter((l) => isDumpLine(l) || /^(@@ |[+-](?![+-])|\| )/.test(l)).length;
|
|
140
|
+
if (dumpy / lines.length >= 0.4)
|
|
96
141
|
return true;
|
|
97
142
|
}
|
|
98
143
|
return false;
|
|
99
144
|
}
|
|
145
|
+
/** Drop dump lines from a live chunk without swallowing normal punctuation. */
|
|
146
|
+
export function stripDumps(text) {
|
|
147
|
+
if (!text)
|
|
148
|
+
return text;
|
|
149
|
+
if (!text.includes("\n"))
|
|
150
|
+
return isDumpLine(text) ? "" : text;
|
|
151
|
+
if (isToolDump(text))
|
|
152
|
+
return "";
|
|
153
|
+
return text
|
|
154
|
+
.split("\n")
|
|
155
|
+
.filter((l) => !isDumpLine(l))
|
|
156
|
+
.join("\n");
|
|
157
|
+
}
|
|
100
158
|
/** Split a reply into short chat bubbles. Drops tool-dump paragraphs. */
|
|
101
159
|
export function splitBubbles(text, max = DEFAULT_MAX_BUBBLE) {
|
|
102
160
|
const cleaned = text.replace(/\r\n/g, "\n").trim();
|
|
@@ -168,39 +226,44 @@ export class TurnPublisher {
|
|
|
168
226
|
published = "";
|
|
169
227
|
toolsUsed = false;
|
|
170
228
|
startTimer = null;
|
|
229
|
+
gapTimer = null;
|
|
171
230
|
seen = new Set();
|
|
172
231
|
startDelayMs;
|
|
173
232
|
maxProgressLines;
|
|
174
233
|
maxBubbleChars;
|
|
234
|
+
bubbleGapMs;
|
|
175
235
|
constructor(sink, opts) {
|
|
176
236
|
this.sink = sink;
|
|
177
237
|
this.opts = opts;
|
|
178
238
|
this.startDelayMs = opts.startDelayMs ?? 400;
|
|
179
239
|
this.maxProgressLines = opts.maxProgressLines ?? 8;
|
|
180
240
|
this.maxBubbleChars = opts.maxBubbleChars ?? DEFAULT_MAX_BUBBLE;
|
|
241
|
+
this.bubbleGapMs = opts.bubbleGapMs ?? 2000;
|
|
181
242
|
}
|
|
182
243
|
onChunk(delta) {
|
|
183
|
-
|
|
244
|
+
const chunk = stripDumps(delta);
|
|
245
|
+
if (!chunk)
|
|
184
246
|
return;
|
|
247
|
+
this.armGap();
|
|
185
248
|
if (this.toolsUsed) {
|
|
186
249
|
this.cancelStart();
|
|
187
|
-
this.enqueue(() => this.appendCommentary(
|
|
250
|
+
this.enqueue(() => this.appendCommentary(chunk));
|
|
188
251
|
return;
|
|
189
252
|
}
|
|
190
253
|
if (this.textP) {
|
|
191
|
-
this.enqueue(() => this.appendCommentary(
|
|
254
|
+
this.enqueue(() => this.appendCommentary(chunk));
|
|
192
255
|
return;
|
|
193
256
|
}
|
|
194
|
-
this.held +=
|
|
257
|
+
this.held += chunk;
|
|
195
258
|
if (this.startTimer)
|
|
196
259
|
return;
|
|
197
260
|
this.startTimer = setTimeout(() => {
|
|
198
261
|
this.startTimer = null;
|
|
199
262
|
if (this.toolsUsed || this.textP || !this.held)
|
|
200
263
|
return;
|
|
201
|
-
const
|
|
264
|
+
const ready = this.held;
|
|
202
265
|
this.held = "";
|
|
203
|
-
this.enqueue(() => this.appendCommentary(
|
|
266
|
+
this.enqueue(() => this.appendCommentary(ready));
|
|
204
267
|
}, this.startDelayMs);
|
|
205
268
|
}
|
|
206
269
|
onTool(ev) {
|
|
@@ -213,10 +276,8 @@ export class TurnPublisher {
|
|
|
213
276
|
return;
|
|
214
277
|
this.seen.add(line);
|
|
215
278
|
this.toolsUsed = true;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.startTimer = null;
|
|
219
|
-
}
|
|
279
|
+
this.cancelStart();
|
|
280
|
+
this.cancelGap();
|
|
220
281
|
this.enqueue(async () => {
|
|
221
282
|
const pending = this.held;
|
|
222
283
|
this.held = "";
|
|
@@ -226,35 +287,27 @@ export class TurnPublisher {
|
|
|
226
287
|
}
|
|
227
288
|
async finish(reply) {
|
|
228
289
|
this.cancelStart();
|
|
290
|
+
this.cancelGap();
|
|
229
291
|
await this.write;
|
|
230
292
|
await this.closeProgress();
|
|
231
|
-
const trimmed = reply.trim();
|
|
232
|
-
if (this.textP && !this.toolsUsed) {
|
|
233
|
-
const s = await this.textP.catch(() => null);
|
|
234
|
-
this.textP = null;
|
|
235
|
-
if (s) {
|
|
236
|
-
const parts = splitBubbles(trimmed || this.textBuf, this.maxBubbleChars);
|
|
237
|
-
const first = parts[0] || trimmed;
|
|
238
|
-
await s.done(first || undefined);
|
|
239
|
-
this.published += first || "";
|
|
240
|
-
for (const extra of parts.slice(1)) {
|
|
241
|
-
this.opts.stopTyping();
|
|
242
|
-
await this.sink.send(extra);
|
|
243
|
-
this.published += `\n\n${extra}`;
|
|
244
|
-
}
|
|
245
|
-
return first ? "streamed" : "empty";
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
293
|
await this.commitText(this.held);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
294
|
+
this.held = "";
|
|
295
|
+
const rest = unpublishedTail(reply.trim(), this.published);
|
|
296
|
+
if (rest) {
|
|
297
|
+
this.opts.stopTyping();
|
|
298
|
+
await this.sendBubbles(rest);
|
|
299
|
+
}
|
|
300
|
+
if (this.toolsUsed && reply.trim() && !isHumanBriefing(this.published)) {
|
|
301
|
+
this.opts.stopTyping();
|
|
302
|
+
await this.sendBubbles(MISSING_BRIEFING);
|
|
303
|
+
}
|
|
304
|
+
if (!this.published.trim())
|
|
305
|
+
return "empty";
|
|
306
|
+
return rest || this.toolsUsed ? "sent" : "streamed";
|
|
255
307
|
}
|
|
256
308
|
async fail(text) {
|
|
257
309
|
this.cancelStart();
|
|
310
|
+
this.cancelGap();
|
|
258
311
|
await this.write;
|
|
259
312
|
await this.closeProgress();
|
|
260
313
|
if (this.textP) {
|
|
@@ -276,6 +329,26 @@ export class TurnPublisher {
|
|
|
276
329
|
clearTimeout(this.startTimer);
|
|
277
330
|
this.startTimer = null;
|
|
278
331
|
}
|
|
332
|
+
cancelGap() {
|
|
333
|
+
if (!this.gapTimer)
|
|
334
|
+
return;
|
|
335
|
+
clearTimeout(this.gapTimer);
|
|
336
|
+
this.gapTimer = null;
|
|
337
|
+
}
|
|
338
|
+
/** After `bubbleGapMs` of silence, seal the current bubble. Next text is new. */
|
|
339
|
+
armGap() {
|
|
340
|
+
this.cancelGap();
|
|
341
|
+
if (this.bubbleGapMs <= 0)
|
|
342
|
+
return;
|
|
343
|
+
this.gapTimer = setTimeout(() => {
|
|
344
|
+
this.gapTimer = null;
|
|
345
|
+
this.enqueue(async () => {
|
|
346
|
+
const pending = this.held;
|
|
347
|
+
this.held = "";
|
|
348
|
+
await this.commitText(pending);
|
|
349
|
+
});
|
|
350
|
+
}, this.bubbleGapMs);
|
|
351
|
+
}
|
|
279
352
|
enqueue(fn) {
|
|
280
353
|
this.write = this.write.then(fn).catch((err) => {
|
|
281
354
|
console.error(`[turn] ${err.message}`);
|
|
@@ -343,8 +416,19 @@ export class TurnPublisher {
|
|
|
343
416
|
if (s && final) {
|
|
344
417
|
if (extra)
|
|
345
418
|
s.append(extra);
|
|
346
|
-
|
|
347
|
-
|
|
419
|
+
const parts = splitBubbles(final, this.maxBubbleChars);
|
|
420
|
+
if (!parts.length) {
|
|
421
|
+
await s.done("…").catch(() => { });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const first = parts[0];
|
|
425
|
+
await s.done(first).catch(() => { });
|
|
426
|
+
this.published += first;
|
|
427
|
+
for (const more of parts.slice(1)) {
|
|
428
|
+
this.opts.stopTyping();
|
|
429
|
+
await this.sink.send(more);
|
|
430
|
+
this.published += `\n\n${more}`;
|
|
431
|
+
}
|
|
348
432
|
return;
|
|
349
433
|
}
|
|
350
434
|
if (final.trim()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nopeek/agent-bridge",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
4
4
|
"description": "Run your own agents as E2EE NoPeek bots. Pairs with one-time codes (multiple accounts per computer), runs every bot each account owns, and pipes messages to any command or webhook.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|