@kybernesis/buzz 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/bridge.d.ts +6 -0
- package/dist/bridge.js +71 -3
- package/dist/cli.js +2 -0
- package/dist/hitl.d.ts +3 -1
- package/dist/hitl.js +7 -3
- package/dist/relay.d.ts +4 -1
- package/dist/relay.js +5 -2
- package/dist/turn.d.ts +3 -0
- package/dist/turn.js +5 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -81,6 +81,20 @@ command on the host. The `.agent` name comes from `ARP_AGENT_DID`, or from
|
|
|
81
81
|
runs it through `@kybernesis/manage` (`buzz.profile`). `kybernesis-buzz profile --from-agentid`
|
|
82
82
|
still takes the profile from the name alone.
|
|
83
83
|
|
|
84
|
+
## Cards
|
|
85
|
+
|
|
86
|
+
When a tool hands back a card (a chart, a table, a checklist, an artifact), the bridge posts it
|
|
87
|
+
after the reply as its own threaded message: the card's text for people to read, and the card
|
|
88
|
+
itself in an HTML comment (`<!-- kyb:card v1 {...} -->`) that Buzz hides. The reply stays an
|
|
89
|
+
ordinary message Buzz draws itself. The Kybernesis cards plugin for the Buzz desktop app
|
|
90
|
+
(`plugin/`) draws those messages as the card; without it people read the text.
|
|
91
|
+
|
|
92
|
+
An artifact posted into a channel is shared with the channel: the bridge asks the agent for a
|
|
93
|
+
share link as the person who asked (`POST /eve/v1/kyb/artifacts/:id/share`, `@kybernesis/manage`
|
|
94
|
+
0.28), and the card carries it. The plugin draws the page inside the card on the macOS desktop
|
|
95
|
+
app and opens it beside the conversation; everywhere else the link is in the text. The person
|
|
96
|
+
can withdraw the link from the agent. `BUZZ_CARDS=off` stops card messages.
|
|
97
|
+
|
|
84
98
|
## Configuration
|
|
85
99
|
|
|
86
100
|
| Variable | Meaning |
|
|
@@ -91,6 +105,7 @@ still takes the profile from the name alone.
|
|
|
91
105
|
| `BUZZ_SESSIONS_FILE` | Channel-to-session continuity store (default beside the key file; an existing legacy `.buzz-sessions.json` is reused) |
|
|
92
106
|
| `BUZZ_AGENT_SILENCE_TIMEOUT_MS` | Maximum silence before the agent acknowledges a request: unread drain, send and create acknowledgement (default `300000`, five minutes). This is where a genuine stall shows: no run is ever created. |
|
|
93
107
|
| `BUZZ_AGENT_WORK_TIMEOUT_MS` | Ceiling on silence once the response stream is open (default `3600000`, sixty minutes). The stream is quiet while the agent works, a long tool call or a delegated subagent produces nothing until it returns, so this is a ceiling on work, not a stall detector. |
|
|
108
|
+
| `BUZZ_CARDS` | `off` stops posting cards after replies (default on) |
|
|
94
109
|
| `BUZZ_PROFILE_SYNC_MS` | How often the profile is re-checked against the name and picture its owner set and its `.agent` name (default `900000`, fifteen minutes; `0` turns it off) |
|
|
95
110
|
| `AGENTID_MIRROR_SUFFIX` | Where names serve their public documents (default `.agentid.dev`); for staging only |
|
|
96
111
|
| `KYBERNESIS_ISSUER` | The control plane |
|
package/dist/bridge.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export type BuzzBridgeOptions = {
|
|
|
23
23
|
agentSilenceTimeoutMs?: number;
|
|
24
24
|
/** Ceiling on a turn once acknowledged; the agent is working while its stream is quiet. Default 60 minutes. */
|
|
25
25
|
agentWorkTimeoutMs?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Post the cards a turn's tools handed back, each as its own message after
|
|
28
|
+
* the reply: the card's text, with the card hidden in a comment for clients
|
|
29
|
+
* that draw it. Default true; false posts the reply alone, as before 0.13.
|
|
30
|
+
*/
|
|
31
|
+
cards?: boolean;
|
|
26
32
|
onLog?: (message: string) => void;
|
|
27
33
|
};
|
|
28
34
|
export declare function buzzBridge(options: BuzzBridgeOptions): {
|
package/dist/bridge.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cardMessage, cardText } from "@kybernesis/cards";
|
|
1
2
|
import { channelIdentity } from "@kybernesis/enterprise";
|
|
2
3
|
import { fetchMedia, parseMedia } from "./media.js";
|
|
3
4
|
import { speakerCredentials } from "./credentials.js";
|
|
@@ -34,6 +35,66 @@ export function buzzBridge(options) {
|
|
|
34
35
|
const log = options.onLog ?? ((message) => console.log(new Date().toISOString().slice(11, 19), message));
|
|
35
36
|
const agentWorkTimeoutMs = validateAgentSilenceTimeoutMs(options.agentWorkTimeoutMs ?? DEFAULT_AGENT_WORK_TIMEOUT_MS);
|
|
36
37
|
const agentSilenceTimeoutMs = validateAgentSilenceTimeoutMs(options.agentSilenceTimeoutMs ?? DEFAULT_AGENT_SILENCE_TIMEOUT_MS);
|
|
38
|
+
/**
|
|
39
|
+
* An artifact card, with a link everyone in the channel can open.
|
|
40
|
+
*
|
|
41
|
+
* Posting an artifact into a channel shares it with the channel: that is
|
|
42
|
+
* what the person asked for by asking here. The link is made by the agent's
|
|
43
|
+
* own share route, as the person who asked (the artifact's owner), so it is
|
|
44
|
+
* their act and they can withdraw it from their artifacts. If the link
|
|
45
|
+
* cannot be made, the card goes out as it was and says where to open it.
|
|
46
|
+
*/
|
|
47
|
+
const sharedInChannel = async (card, speaker) => {
|
|
48
|
+
if (card.card !== "artifact" || !card.data || typeof card.data !== "object")
|
|
49
|
+
return card;
|
|
50
|
+
const data = card.data;
|
|
51
|
+
if (typeof data.artifactId !== "string" || typeof data.url === "string")
|
|
52
|
+
return card;
|
|
53
|
+
try {
|
|
54
|
+
const credentials = credentialsFor(speaker);
|
|
55
|
+
const response = await fetch(`${options.agentUrl.replace(/\/$/, "")}/eve/v1/kyb/artifacts/${encodeURIComponent(data.artifactId)}/share`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: {
|
|
58
|
+
...(await credentials.headers()),
|
|
59
|
+
authorization: `Bearer ${await credentials.bearer()}`,
|
|
60
|
+
"content-type": "application/json",
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify(typeof data.version === "number" ? { version: data.version } : {}),
|
|
63
|
+
signal: AbortSignal.timeout(15_000),
|
|
64
|
+
});
|
|
65
|
+
const body = (await response.json().catch(() => null));
|
|
66
|
+
if (!response.ok || !body?.ok || typeof body.url !== "string") {
|
|
67
|
+
log(`could not share artifact ${data.artifactId.slice(0, 8)} (${response.status} ${String(body?.error ?? "")})`);
|
|
68
|
+
return card;
|
|
69
|
+
}
|
|
70
|
+
// The words people without the plugin read now carry the link.
|
|
71
|
+
const shared = { ...card, data: { ...data, url: body.url }, text: undefined };
|
|
72
|
+
return { ...shared, text: cardText(shared) };
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
log(`could not share artifact ${data.artifactId.slice(0, 8)} (${error.message})`);
|
|
76
|
+
return card;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* A turn's cards, one message each, threaded where the reply went.
|
|
81
|
+
*
|
|
82
|
+
* The reply stays an ordinary message the client draws itself, links and
|
|
83
|
+
* mentions included; a card-aware client swaps only these follow-ups for the
|
|
84
|
+
* card. They do not mention the asker again: the reply already did.
|
|
85
|
+
*/
|
|
86
|
+
const postCards = async (relay, channel, cards, speaker, replyTo) => {
|
|
87
|
+
if (options.cards === false || cards.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
let posted = 0;
|
|
90
|
+
for (const card of await Promise.all(cards.map((c) => sharedInChannel(c, speaker)))) {
|
|
91
|
+
const content = cardMessage(card);
|
|
92
|
+
if (content && relay.reply(channel, content, replyTo, { mention: false }))
|
|
93
|
+
posted += 1;
|
|
94
|
+
}
|
|
95
|
+
if (posted)
|
|
96
|
+
log(`posted ${posted} card(s) in ${channel.slice(0, 8)}`);
|
|
97
|
+
};
|
|
37
98
|
/**
|
|
38
99
|
* One conversation per channel — the SESSION ID, not a handle to it.
|
|
39
100
|
*
|
|
@@ -249,12 +310,14 @@ export function buzzBridge(options) {
|
|
|
249
310
|
}
|
|
250
311
|
log(`posted another input request in ${channel.slice(0, 8)}`);
|
|
251
312
|
}),
|
|
252
|
-
onMessage: (message) => serialize(community, channel, async () => {
|
|
313
|
+
onMessage: (message, cards) => serialize(community, channel, async () => {
|
|
253
314
|
if (followers.get(conversation) !== controller)
|
|
254
315
|
return;
|
|
255
|
-
if (!relay.reply(channel, message))
|
|
316
|
+
if (message.trim() && !relay.reply(channel, message))
|
|
256
317
|
throw new Error("Buzz relay is not connected");
|
|
257
|
-
|
|
318
|
+
if (message.trim())
|
|
319
|
+
log(`posted resumed reply in ${channel.slice(0, 8)} (${message.length} chars)`);
|
|
320
|
+
await postCards(relay, channel, cards, stored.speakerPublicKey);
|
|
258
321
|
}),
|
|
259
322
|
onLog: log,
|
|
260
323
|
}, controller.signal).catch((error) => {
|
|
@@ -422,6 +485,10 @@ export function buzzBridge(options) {
|
|
|
422
485
|
startFollower(from, channel);
|
|
423
486
|
return;
|
|
424
487
|
}
|
|
488
|
+
if (!result.message && result.cards.length) {
|
|
489
|
+
await postCards(relay, channel, result.cards, event.pubkey, event);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
425
492
|
if (!result.message) {
|
|
426
493
|
// Say what the stream said: a failed tool by name and error, a
|
|
427
494
|
// failed model call by code, or, only when there is truly nothing,
|
|
@@ -432,6 +499,7 @@ export function buzzBridge(options) {
|
|
|
432
499
|
}
|
|
433
500
|
relay.reply(channel, result.message, event);
|
|
434
501
|
log(`replied (${result.message.length} chars)`);
|
|
502
|
+
await postCards(relay, channel, result.cards, event.pubkey, event);
|
|
435
503
|
});
|
|
436
504
|
}
|
|
437
505
|
catch (error) {
|
package/dist/cli.js
CHANGED
|
@@ -230,6 +230,7 @@ function run() {
|
|
|
230
230
|
...(process.env.BUZZ_AGENT_WORK_TIMEOUT_MS
|
|
231
231
|
? { agentWorkTimeoutMs: Number(process.env.BUZZ_AGENT_WORK_TIMEOUT_MS) }
|
|
232
232
|
: {}),
|
|
233
|
+
...(process.env.BUZZ_CARDS === "off" ? { cards: false } : {}),
|
|
233
234
|
...(process.env.BUZZ_AGENT_SILENCE_TIMEOUT_MS
|
|
234
235
|
? { agentSilenceTimeoutMs: Number(process.env.BUZZ_AGENT_SILENCE_TIMEOUT_MS) }
|
|
235
236
|
: {}),
|
|
@@ -422,6 +423,7 @@ else {
|
|
|
422
423
|
BUZZ_SESSIONS_FILE channel-to-session continuity store
|
|
423
424
|
BUZZ_AGENT_SILENCE_TIMEOUT_MS maximum silence before a turn is acknowledged (default 300000ms)
|
|
424
425
|
BUZZ_AGENT_WORK_TIMEOUT_MS ceiling on a turn once acknowledged (default 3600000ms)
|
|
426
|
+
BUZZ_CARDS=off post replies without the cards tools handed back
|
|
425
427
|
BUZZ_PROFILE_SYNC_MS how often the profile is re-checked against the name and
|
|
426
428
|
picture its owner set (default 900000ms, fifteen
|
|
427
429
|
minutes; 0 turns it off)
|
package/dist/hitl.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ClientSession, type InputRequest, type InputResponse } from "eve/client";
|
|
2
|
+
import { type Card } from "@kybernesis/cards";
|
|
2
3
|
import type { StoredSession } from "./sessions.js";
|
|
3
4
|
/**
|
|
4
5
|
* Render pending Eve input in a text-only Buzz channel.
|
|
@@ -39,7 +40,8 @@ export interface PendingFollowerCallbacks {
|
|
|
39
40
|
/** Persist the latest cursor and supervision state before publishing outward. */
|
|
40
41
|
onState(state: Omit<StoredSession, "updated">): void | Promise<void>;
|
|
41
42
|
onInputRequested(requests: readonly InputRequest[]): void | Promise<void>;
|
|
42
|
-
|
|
43
|
+
/** The resumed turn's words, and the cards its tools handed back. */
|
|
44
|
+
onMessage(message: string, cards: readonly Card[]): void | Promise<void>;
|
|
43
45
|
onLog?: (message: string) => void;
|
|
44
46
|
}
|
|
45
47
|
type MutableStoredSession = Omit<StoredSession, "updated">;
|
package/dist/hitl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveTextToResponses, } from "eve/client";
|
|
2
|
-
import { promptText } from "@kybernesis/cards";
|
|
2
|
+
import { cardsOf, newStepState, promptText, readStep } from "@kybernesis/cards";
|
|
3
3
|
/**
|
|
4
4
|
* Render pending Eve input in a text-only Buzz channel.
|
|
5
5
|
*
|
|
@@ -105,6 +105,7 @@ export async function followPendingConversation(session, initial, callbacks, sig
|
|
|
105
105
|
let state = { ...initial };
|
|
106
106
|
let streamIndex = initial.streamIndex;
|
|
107
107
|
let completedMessage = "";
|
|
108
|
+
let steps = newStepState();
|
|
108
109
|
const persist = async () => {
|
|
109
110
|
state = { ...state, streamIndex };
|
|
110
111
|
await callbacks.onState(state);
|
|
@@ -113,8 +114,9 @@ export async function followPendingConversation(session, initial, callbacks, sig
|
|
|
113
114
|
// Do not persist past message.completed/turn.completed until publication
|
|
114
115
|
// succeeds. If this callback throws or the process dies, recovery replays
|
|
115
116
|
// the unpublished output from the last safe cursor.
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
const cards = cardsOf(steps.steps.values());
|
|
118
|
+
if (completedMessage.trim() || cards.length)
|
|
119
|
+
await callbacks.onMessage(completedMessage, cards);
|
|
118
120
|
state = withoutSupervision(state);
|
|
119
121
|
await persist();
|
|
120
122
|
};
|
|
@@ -130,9 +132,11 @@ export async function followPendingConversation(session, initial, callbacks, sig
|
|
|
130
132
|
// closes; count yielded durable events so crash recovery can persist while
|
|
131
133
|
// the follower is still open.
|
|
132
134
|
streamIndex += 1;
|
|
135
|
+
readStep(steps, streamEvent.type, streamEvent.data);
|
|
133
136
|
switch (streamEvent.type) {
|
|
134
137
|
case "input.requested":
|
|
135
138
|
completedMessage = "";
|
|
139
|
+
steps = newStepState();
|
|
136
140
|
state = withPending(state, streamEvent.data.requests);
|
|
137
141
|
// Keep the durable cursor before the prompt until Buzz accepts it for
|
|
138
142
|
// relay delivery; a reconnect can then replay and render the request.
|
package/dist/relay.d.ts
CHANGED
|
@@ -144,7 +144,10 @@ export declare class BuzzRelay {
|
|
|
144
144
|
private keepPresent;
|
|
145
145
|
/** Say "typing" in a channel until the returned function is called. */
|
|
146
146
|
typingIn(channel: string): () => void;
|
|
147
|
-
|
|
147
|
+
/** `mention: false` threads under `replyTo` without notifying its author again, as a follow-up to a reply does. */
|
|
148
|
+
reply(channel: string, text: string, replyTo?: NostrEvent, options?: {
|
|
149
|
+
mention?: boolean;
|
|
150
|
+
}): string | null;
|
|
148
151
|
/** Sign one HTTP request as this agent (NIP-98), binding the signature to the body. */
|
|
149
152
|
private httpAuth;
|
|
150
153
|
private submit;
|
package/dist/relay.js
CHANGED
|
@@ -425,10 +425,13 @@ export class BuzzRelay {
|
|
|
425
425
|
const timer = setInterval(tick, TYPING_INTERVAL_MS);
|
|
426
426
|
return () => clearInterval(timer);
|
|
427
427
|
}
|
|
428
|
-
|
|
428
|
+
/** `mention: false` threads under `replyTo` without notifying its author again, as a follow-up to a reply does. */
|
|
429
|
+
reply(channel, text, replyTo, options = {}) {
|
|
429
430
|
const tags = [["h", channel]];
|
|
430
431
|
if (replyTo) {
|
|
431
|
-
tags.push(["e", replyTo.id]
|
|
432
|
+
tags.push(["e", replyTo.id]);
|
|
433
|
+
if (options.mention !== false)
|
|
434
|
+
tags.push(["p", replyTo.pubkey]);
|
|
432
435
|
}
|
|
433
436
|
return this.publish({ kind: KIND_MESSAGE, tags, content: text });
|
|
434
437
|
}
|
package/dist/turn.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Client, type InputRequest, type MessageResult, type SendTurnInput } from "eve/client";
|
|
2
|
+
import { type Card } from "@kybernesis/cards";
|
|
2
3
|
import { type FetchedMedia, type MediaRef } from "./media.js";
|
|
3
4
|
import { SessionStore } from "./sessions.js";
|
|
4
5
|
/** Five minutes without an eve event or acknowledgement is treated as a stalled turn. */
|
|
@@ -64,6 +65,8 @@ export interface TurnOutcome {
|
|
|
64
65
|
streamIndex: number;
|
|
65
66
|
/** Failures seen on the stream, in order. Empty on a clean turn. */
|
|
66
67
|
failures: readonly TurnFailure[];
|
|
68
|
+
/** Cards the turn's tools handed back, read the way the apps read them. */
|
|
69
|
+
cards: readonly Card[];
|
|
67
70
|
}
|
|
68
71
|
/** Read a failure out of one stream event, or nothing when the event is not one. */
|
|
69
72
|
export declare function failureFromEvent(event: {
|
package/dist/turn.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ClientError, createDataUrlFilePart } from "eve/client";
|
|
2
|
+
import { cardsOf, newStepState, readStep } from "@kybernesis/cards";
|
|
2
3
|
import { isImage } from "./media.js";
|
|
3
4
|
/** Five minutes without an eve event or acknowledgement is treated as a stalled turn. */
|
|
4
5
|
export const DEFAULT_AGENT_SILENCE_TIMEOUT_MS = 5 * 60_000;
|
|
@@ -210,6 +211,7 @@ async function collectResponse(response, watchdog, phase, streamIndex, continueS
|
|
|
210
211
|
let status = "completed";
|
|
211
212
|
let inputRequests = [];
|
|
212
213
|
let failures = [];
|
|
214
|
+
let steps = newStepState();
|
|
213
215
|
let sawTurnStart = false;
|
|
214
216
|
let endedAtOwnBoundary = false;
|
|
215
217
|
let staleBoundaryFirst = false;
|
|
@@ -240,8 +242,10 @@ async function collectResponse(response, watchdog, phase, streamIndex, continueS
|
|
|
240
242
|
status = "completed";
|
|
241
243
|
inputRequests = [];
|
|
242
244
|
failures = [];
|
|
245
|
+
steps = newStepState();
|
|
243
246
|
return false;
|
|
244
247
|
}
|
|
248
|
+
readStep(steps, event.type, event.data);
|
|
245
249
|
const failure = failureFromEvent(event);
|
|
246
250
|
if (failure)
|
|
247
251
|
failures.push(failure);
|
|
@@ -298,6 +302,7 @@ async function collectResponse(response, watchdog, phase, streamIndex, continueS
|
|
|
298
302
|
sessionId: response.sessionId,
|
|
299
303
|
streamIndex: streamIndex(),
|
|
300
304
|
failures,
|
|
305
|
+
cards: cardsOf(steps.steps.values()),
|
|
301
306
|
};
|
|
302
307
|
}
|
|
303
308
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/buzz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Put an eve agent in a Buzz workspace as a member: per-speaker verified identity resolved through the control plane, self-service identity linking, presence, typing and seen signals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"test": "node --test \"test/**/*.test.mjs\""
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@kybernesis/cards": "^0.
|
|
47
|
+
"@kybernesis/cards": "^0.4.0",
|
|
48
48
|
"@kybernesis/enterprise": "^0.10.0",
|
|
49
49
|
"nostr-tools": "^2.10.0",
|
|
50
50
|
"zod": "^4"
|