@butlerbot/sdk 0.0.31 → 0.0.33
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/link/agent.d.ts +103 -0
- package/dist/link/agent.js +86 -0
- package/dist/link/index.d.ts +3 -1
- package/dist/link/index.js +3 -1
- package/dist/link/link.d.ts +24 -1
- package/dist/link/link.js +77 -1
- package/dist/link/protocol.d.ts +47 -0
- package/dist/types/conversation/v3/conversation_v3.d.ts +10 -0
- package/package.json +1 -1
- package/readme.md +39 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { LinkAgentDescriptor, LinkServerFrame, LinkServerFrameOf } from "./protocol";
|
|
2
|
+
import { AnyTool } from "./tool";
|
|
3
|
+
export type AgentConfig = {
|
|
4
|
+
/**
|
|
5
|
+
* This agent's id within the link. The public id becomes `link:<linkId>/<id>`,
|
|
6
|
+
* which is what the user's saved settings refer to — so treat it as permanent.
|
|
7
|
+
*/
|
|
8
|
+
id: string;
|
|
9
|
+
/** The agent's own name. It signs its replies with it. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** What Alfred reads when deciding whether to hand something to this agent. */
|
|
12
|
+
description: string;
|
|
13
|
+
/**
|
|
14
|
+
* The agent's system prompt: who it is, what its tools are for, and how they go together.
|
|
15
|
+
* This is where a thousand tools become one coherent worker.
|
|
16
|
+
*/
|
|
17
|
+
prompt: string;
|
|
18
|
+
/**
|
|
19
|
+
* The model the agent runs on, by its catalogue name. Omit it for the user's default.
|
|
20
|
+
* A model the user's plan does not include falls back to their default, exactly as a chat would.
|
|
21
|
+
*/
|
|
22
|
+
model?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The tools the agent works with. They belong to the agent: Alfred never sees them
|
|
25
|
+
* directly, and they need no `addTool` of their own.
|
|
26
|
+
*/
|
|
27
|
+
tools?: AnyTool[];
|
|
28
|
+
/** Shown in Alfred's settings UI. Without it the agent is hidden there. */
|
|
29
|
+
display?: {
|
|
30
|
+
name: string;
|
|
31
|
+
shortDescription: string;
|
|
32
|
+
longDescription: string;
|
|
33
|
+
};
|
|
34
|
+
/** Whether the agent is on before the user has touched it. */
|
|
35
|
+
defaultEnabled?: boolean;
|
|
36
|
+
};
|
|
37
|
+
/** Progress on an exchange, in the words the agent's status feed would show a conversation. */
|
|
38
|
+
export type AgentStatus = {
|
|
39
|
+
label: string;
|
|
40
|
+
state: "running" | "completed" | "failed";
|
|
41
|
+
};
|
|
42
|
+
export type AgentChatOptions = {
|
|
43
|
+
/**
|
|
44
|
+
* The exchange this message continues. Messages on one thread share memory.
|
|
45
|
+
*
|
|
46
|
+
* Defaults to the agent's own thread, which is minted when the `Agent` is created — so one
|
|
47
|
+
* `Agent` remembers across `chat` calls, and a new process starts afresh. Name one yourself
|
|
48
|
+
* to pick a conversation up across restarts, or to keep several going at once.
|
|
49
|
+
*/
|
|
50
|
+
thread?: string;
|
|
51
|
+
/** Called with each status update while the agent works. */
|
|
52
|
+
onStatus?: (status: AgentStatus) => void;
|
|
53
|
+
};
|
|
54
|
+
export type AgentReply = {
|
|
55
|
+
/** What the agent said. */
|
|
56
|
+
text: string;
|
|
57
|
+
/** The thread the reply belongs to, which is what continues it. */
|
|
58
|
+
thread: string;
|
|
59
|
+
};
|
|
60
|
+
/** What an agent needs from its link. Implemented by `Link`. */
|
|
61
|
+
export type AgentRunner = {
|
|
62
|
+
chatAgent(agentId: string, message: string, options: AgentChatOptions): Promise<AgentReply>;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* An AI worker that lives on the server for as long as the link does, working with tools
|
|
66
|
+
* that run here.
|
|
67
|
+
*
|
|
68
|
+
* To Alfred it is one tool: he hands it a task and gets a reply, and the tools behind it
|
|
69
|
+
* stay behind it. To your code it is something to talk to directly — `chat` runs it on the
|
|
70
|
+
* server and hands the answer back here, so a hook callback can ask it to decide something
|
|
71
|
+
* and act on what it says.
|
|
72
|
+
*/
|
|
73
|
+
export declare class Agent {
|
|
74
|
+
private readonly config;
|
|
75
|
+
readonly id: string;
|
|
76
|
+
readonly tools: AnyTool[];
|
|
77
|
+
/** The public id (`link:<linkId>/<id>`), known once the link has registered it. */
|
|
78
|
+
linkedId?: string;
|
|
79
|
+
/** The thread `chat` uses when not told otherwise. One per `Agent`, so it remembers. */
|
|
80
|
+
thread: string;
|
|
81
|
+
private link?;
|
|
82
|
+
constructor(config: AgentConfig);
|
|
83
|
+
get name(): string;
|
|
84
|
+
get description(): string;
|
|
85
|
+
/** The declaration sent to the server. */
|
|
86
|
+
descriptor(): LinkAgentDescriptor;
|
|
87
|
+
/** Called by `Link.addAgent`. */
|
|
88
|
+
attach(link: AgentRunner): void;
|
|
89
|
+
/** One of this agent's tools, by its id. */
|
|
90
|
+
getTool(id: string): AnyTool | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Asks the agent something and waits for its reply.
|
|
93
|
+
*
|
|
94
|
+
* The run happens on the server, on the user's account; the question and the answer live
|
|
95
|
+
* here. Rejects with a `LinkError` when the agent could not run or failed — never with the
|
|
96
|
+
* agent's own prose, which is a reply like any other.
|
|
97
|
+
*/
|
|
98
|
+
chat(message: string, options?: AgentChatOptions): Promise<AgentReply>;
|
|
99
|
+
/** Starts a fresh thread for the calls that follow, and returns it. */
|
|
100
|
+
newThread(): string;
|
|
101
|
+
}
|
|
102
|
+
/** Whether a server frame ends an `agent.chat` exchange. */
|
|
103
|
+
export declare function isAgentResult(frame: LinkServerFrame): frame is LinkServerFrameOf<"agent.result">;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Agent = void 0;
|
|
4
|
+
exports.isAgentResult = isAgentResult;
|
|
5
|
+
/**
|
|
6
|
+
* An AI worker that lives on the server for as long as the link does, working with tools
|
|
7
|
+
* that run here.
|
|
8
|
+
*
|
|
9
|
+
* To Alfred it is one tool: he hands it a task and gets a reply, and the tools behind it
|
|
10
|
+
* stay behind it. To your code it is something to talk to directly — `chat` runs it on the
|
|
11
|
+
* server and hands the answer back here, so a hook callback can ask it to decide something
|
|
12
|
+
* and act on what it says.
|
|
13
|
+
*/
|
|
14
|
+
class Agent {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.id = config.id;
|
|
18
|
+
this.tools = [...(config.tools ?? [])];
|
|
19
|
+
this.thread = mintThread();
|
|
20
|
+
const ids = new Set();
|
|
21
|
+
for (const tool of this.tools) {
|
|
22
|
+
if (ids.has(tool.id))
|
|
23
|
+
throw new Error(`Agent "${this.id}" holds two tools with the id "${tool.id}".`);
|
|
24
|
+
ids.add(tool.id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get name() {
|
|
28
|
+
return this.config.name;
|
|
29
|
+
}
|
|
30
|
+
get description() {
|
|
31
|
+
return this.config.description;
|
|
32
|
+
}
|
|
33
|
+
/** The declaration sent to the server. */
|
|
34
|
+
descriptor() {
|
|
35
|
+
return {
|
|
36
|
+
localId: this.id,
|
|
37
|
+
name: this.config.name,
|
|
38
|
+
description: this.config.description,
|
|
39
|
+
prompt: this.config.prompt,
|
|
40
|
+
...(this.config.model ? { model: this.config.model } : {}),
|
|
41
|
+
// An agent's tools are placed by the agent, so whatever platform a tool
|
|
42
|
+
// declared for itself does not travel.
|
|
43
|
+
tools: this.tools.map(tool => {
|
|
44
|
+
const { platforms: _platforms, ...descriptor } = tool.descriptor();
|
|
45
|
+
return descriptor;
|
|
46
|
+
}),
|
|
47
|
+
...(this.config.display ? { display: this.config.display } : {}),
|
|
48
|
+
...(this.config.defaultEnabled !== undefined ? { defaultEnabled: this.config.defaultEnabled } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Called by `Link.addAgent`. */
|
|
52
|
+
attach(link) {
|
|
53
|
+
this.link = link;
|
|
54
|
+
}
|
|
55
|
+
/** One of this agent's tools, by its id. */
|
|
56
|
+
getTool(id) {
|
|
57
|
+
return this.tools.find(tool => tool.id === id);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Asks the agent something and waits for its reply.
|
|
61
|
+
*
|
|
62
|
+
* The run happens on the server, on the user's account; the question and the answer live
|
|
63
|
+
* here. Rejects with a `LinkError` when the agent could not run or failed — never with the
|
|
64
|
+
* agent's own prose, which is a reply like any other.
|
|
65
|
+
*/
|
|
66
|
+
chat(message, options = {}) {
|
|
67
|
+
if (!this.link)
|
|
68
|
+
return Promise.reject(new Error(`Agent "${this.id}" has not been added to a link.`));
|
|
69
|
+
return this.link.chatAgent(this.id, message, { thread: this.thread, ...options });
|
|
70
|
+
}
|
|
71
|
+
/** Starts a fresh thread for the calls that follow, and returns it. */
|
|
72
|
+
newThread() {
|
|
73
|
+
this.thread = mintThread();
|
|
74
|
+
return this.thread;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.Agent = Agent;
|
|
78
|
+
/** A thread id the server will accept: letters, digits, dot, dash, underscore, 64 at most. */
|
|
79
|
+
function mintThread() {
|
|
80
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
81
|
+
return `t-${Date.now().toString(36)}-${random}`;
|
|
82
|
+
}
|
|
83
|
+
/** Whether a server frame ends an `agent.chat` exchange. */
|
|
84
|
+
function isAgentResult(frame) {
|
|
85
|
+
return frame.type === "agent.result";
|
|
86
|
+
}
|
package/dist/link/index.d.ts
CHANGED
|
@@ -2,10 +2,12 @@ export { Link } from "./link";
|
|
|
2
2
|
export type { LinkOptions, LinkEvents, LinkState, ExchangeOptions } from "./link";
|
|
3
3
|
export { Tool } from "./tool";
|
|
4
4
|
export type { AnyTool, ToolConfig, ToolRunContext, ToolCallMeta, ToolStatusReporter, ToolInvocation } from "./tool";
|
|
5
|
+
export { Agent } from "./agent";
|
|
6
|
+
export type { AgentConfig, AgentChatOptions, AgentReply, AgentStatus, AgentRunner } from "./agent";
|
|
5
7
|
export { Hook } from "./hook";
|
|
6
8
|
export type { AnyHook, HookConfig, HookEmitter } from "./hook";
|
|
7
9
|
export { LINK_PROTOCOL_VERSION, LinkError } from "./protocol";
|
|
8
|
-
export type { LinkClientFrame, LinkClientFrameOf, LinkClientFrameType, LinkServerFrame, LinkServerFrameOf, LinkServerFrameType, LinkScopeKind, LinkToolDescriptor, LinkHookDeclaration, LinkHookEventDeclaration, } from "./protocol";
|
|
10
|
+
export type { LinkClientFrame, LinkClientFrameOf, LinkClientFrameType, LinkServerFrame, LinkServerFrameOf, LinkServerFrameType, LinkScopeKind, LinkToolDescriptor, LinkAgentDescriptor, LinkHookDeclaration, LinkHookEventDeclaration, } from "./protocol";
|
|
9
11
|
export { SubscriptionStore } from "./subscriptions";
|
|
10
12
|
export type { LinkSubscription, SubscriptionSnapshot, SubscriptionDelta } from "./subscriptions";
|
|
11
13
|
export { matchesPrefilter, readPath } from "./prefilter";
|
package/dist/link/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.defaultSocketFactory = exports.buildHandshake = exports.readPath = exports.matchesPrefilter = exports.SubscriptionStore = exports.LinkError = exports.LINK_PROTOCOL_VERSION = exports.Hook = exports.Tool = exports.Link = void 0;
|
|
3
|
+
exports.defaultSocketFactory = exports.buildHandshake = exports.readPath = exports.matchesPrefilter = exports.SubscriptionStore = exports.LinkError = exports.LINK_PROTOCOL_VERSION = exports.Hook = exports.Agent = exports.Tool = exports.Link = void 0;
|
|
4
4
|
var link_1 = require("./link");
|
|
5
5
|
Object.defineProperty(exports, "Link", { enumerable: true, get: function () { return link_1.Link; } });
|
|
6
6
|
var tool_1 = require("./tool");
|
|
7
7
|
Object.defineProperty(exports, "Tool", { enumerable: true, get: function () { return tool_1.Tool; } });
|
|
8
|
+
var agent_1 = require("./agent");
|
|
9
|
+
Object.defineProperty(exports, "Agent", { enumerable: true, get: function () { return agent_1.Agent; } });
|
|
8
10
|
var hook_1 = require("./hook");
|
|
9
11
|
Object.defineProperty(exports, "Hook", { enumerable: true, get: function () { return hook_1.Hook; } });
|
|
10
12
|
var protocol_1 = require("./protocol");
|
package/dist/link/link.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Agent, AgentChatOptions, AgentReply, AgentRunner } from "./agent";
|
|
1
2
|
import { AnyHook } from "./hook";
|
|
2
3
|
import { LinkClientFrameType, LinkClientPayloads, LinkScopeKind, LinkServerFrame } from "./protocol";
|
|
3
4
|
import { SocketFactory } from "./socket";
|
|
@@ -83,10 +84,11 @@ export type ExchangeOptions = {
|
|
|
83
84
|
* re-declared on connect, and ids are derived from your `linkId`, so a reconnect
|
|
84
85
|
* anywhere lands on the same saved settings and subscriptions.
|
|
85
86
|
*/
|
|
86
|
-
export declare class Link {
|
|
87
|
+
export declare class Link implements AgentRunner {
|
|
87
88
|
private readonly options;
|
|
88
89
|
private readonly emitter;
|
|
89
90
|
private readonly tools;
|
|
91
|
+
private readonly agents;
|
|
90
92
|
private readonly hooks;
|
|
91
93
|
private readonly subscriptionStore;
|
|
92
94
|
private readonly pending;
|
|
@@ -129,10 +131,22 @@ export declare class Link {
|
|
|
129
131
|
constructor(options: LinkOptions);
|
|
130
132
|
/** Adds a tool Alfred can call. Registered on connect, or immediately if already open. */
|
|
131
133
|
addTool(tool: AnyTool): this;
|
|
134
|
+
/**
|
|
135
|
+
* Adds an agent, with the tools it works with.
|
|
136
|
+
*
|
|
137
|
+
* The tools come with the agent — they need no `addTool` of their own, and giving them one
|
|
138
|
+
* would place them in the chat as well, which is the thing an agent exists to avoid. One
|
|
139
|
+
* namespace for everything the link declares, so an id used twice is refused here rather
|
|
140
|
+
* than resolved by whichever registered last.
|
|
141
|
+
*/
|
|
142
|
+
addAgent(agent: Agent): this;
|
|
132
143
|
/** Adds a hook that can wake the user's background agents. */
|
|
133
144
|
addHook(hook: AnyHook): this;
|
|
134
145
|
getTool(id: string): AnyTool | undefined;
|
|
135
146
|
getHook(id: string): AnyHook | undefined;
|
|
147
|
+
getAgent(id: string): Agent | undefined;
|
|
148
|
+
/** A tool by its local id, wherever it lives: on the link itself or behind one of its agents. */
|
|
149
|
+
private findTool;
|
|
136
150
|
get state(): LinkState;
|
|
137
151
|
get linkId(): string;
|
|
138
152
|
/** The ephemeral id of this connection. Changes on every reconnect. */
|
|
@@ -244,6 +258,15 @@ export declare class Link {
|
|
|
244
258
|
private wait;
|
|
245
259
|
private settleWaiters;
|
|
246
260
|
private registerAll;
|
|
261
|
+
private registerAgents;
|
|
262
|
+
/**
|
|
263
|
+
* Called by `Agent.chat`.
|
|
264
|
+
*
|
|
265
|
+
* One exchange, however long the agent takes: status frames go to the caller as they
|
|
266
|
+
* arrive, and the result frame ends it. Never timed out here — an agent that is calling
|
|
267
|
+
* tools on this very machine may legitimately take a while.
|
|
268
|
+
*/
|
|
269
|
+
chatAgent(agentId: string, message: string, options: AgentChatOptions): Promise<AgentReply>;
|
|
247
270
|
private registerTools;
|
|
248
271
|
private registerHook;
|
|
249
272
|
/**
|
package/dist/link/link.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Link = void 0;
|
|
4
4
|
const config_1 = require("../config");
|
|
5
5
|
const emitter_1 = require("../util/emitter");
|
|
6
|
+
const agent_1 = require("./agent");
|
|
6
7
|
const protocol_1 = require("./protocol");
|
|
7
8
|
const socket_1 = require("./socket");
|
|
8
9
|
const subscriptions_1 = require("./subscriptions");
|
|
@@ -23,6 +24,7 @@ class Link {
|
|
|
23
24
|
constructor(options) {
|
|
24
25
|
this.emitter = new emitter_1.Emitter();
|
|
25
26
|
this.tools = new Map();
|
|
27
|
+
this.agents = new Map();
|
|
26
28
|
this.hooks = new Map();
|
|
27
29
|
this.subscriptionStore = new subscriptions_1.SubscriptionStore();
|
|
28
30
|
this.pending = new Map();
|
|
@@ -93,6 +95,29 @@ class Link {
|
|
|
93
95
|
void this.registerTools([tool]);
|
|
94
96
|
return this;
|
|
95
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Adds an agent, with the tools it works with.
|
|
100
|
+
*
|
|
101
|
+
* The tools come with the agent — they need no `addTool` of their own, and giving them one
|
|
102
|
+
* would place them in the chat as well, which is the thing an agent exists to avoid. One
|
|
103
|
+
* namespace for everything the link declares, so an id used twice is refused here rather
|
|
104
|
+
* than resolved by whichever registered last.
|
|
105
|
+
*/
|
|
106
|
+
addAgent(agent) {
|
|
107
|
+
for (const tool of agent.tools) {
|
|
108
|
+
const holder = this.findTool(tool.id) ? "another tool" : this.agents.has(tool.id) ? "an agent" : undefined;
|
|
109
|
+
if (holder)
|
|
110
|
+
throw new Error(`Cannot add agent "${agent.id}": its tool "${tool.id}" shares an id with ${holder} on this link.`);
|
|
111
|
+
}
|
|
112
|
+
if (this.tools.has(agent.id) || this.findTool(agent.id)) {
|
|
113
|
+
throw new Error(`Cannot add agent "${agent.id}": a tool on this link already has that id.`);
|
|
114
|
+
}
|
|
115
|
+
this.agents.set(agent.id, agent);
|
|
116
|
+
agent.attach(this);
|
|
117
|
+
if (this.currentState === "open")
|
|
118
|
+
void this.registerAgents([agent]);
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
96
121
|
/** Adds a hook that can wake the user's background agents. */
|
|
97
122
|
addHook(hook) {
|
|
98
123
|
this.hooks.set(hook.id, hook);
|
|
@@ -107,6 +132,21 @@ class Link {
|
|
|
107
132
|
getHook(id) {
|
|
108
133
|
return this.hooks.get(id);
|
|
109
134
|
}
|
|
135
|
+
getAgent(id) {
|
|
136
|
+
return this.agents.get(id);
|
|
137
|
+
}
|
|
138
|
+
/** A tool by its local id, wherever it lives: on the link itself or behind one of its agents. */
|
|
139
|
+
findTool(localId) {
|
|
140
|
+
const own = this.tools.get(localId);
|
|
141
|
+
if (own)
|
|
142
|
+
return own;
|
|
143
|
+
for (const agent of this.agents.values()) {
|
|
144
|
+
const tool = agent.getTool(localId);
|
|
145
|
+
if (tool)
|
|
146
|
+
return tool;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
110
150
|
// =============================================
|
|
111
151
|
// STATE
|
|
112
152
|
// =============================================
|
|
@@ -516,9 +556,45 @@ class Link {
|
|
|
516
556
|
// =============================================
|
|
517
557
|
async registerAll() {
|
|
518
558
|
await this.registerTools(Array.from(this.tools.values()));
|
|
559
|
+
await this.registerAgents(Array.from(this.agents.values()));
|
|
519
560
|
for (const hook of this.hooks.values())
|
|
520
561
|
await this.registerHook(hook);
|
|
521
562
|
}
|
|
563
|
+
async registerAgents(agents) {
|
|
564
|
+
if (!agents.length)
|
|
565
|
+
return;
|
|
566
|
+
const frame = await this.exchange("agent.register", { agents: agents.map(agent => agent.descriptor()) }, { awaitReady: false });
|
|
567
|
+
const ids = frame.payload.ids ?? [];
|
|
568
|
+
agents.forEach((agent, index) => { agent.linkedId = ids[index]; });
|
|
569
|
+
this.debug(`registered ${agents.length} agent(s)`);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Called by `Agent.chat`.
|
|
573
|
+
*
|
|
574
|
+
* One exchange, however long the agent takes: status frames go to the caller as they
|
|
575
|
+
* arrive, and the result frame ends it. Never timed out here — an agent that is calling
|
|
576
|
+
* tools on this very machine may legitimately take a while.
|
|
577
|
+
*/
|
|
578
|
+
async chatAgent(agentId, message, options) {
|
|
579
|
+
const frame = await this.exchange("agent.chat", {
|
|
580
|
+
localId: agentId,
|
|
581
|
+
message,
|
|
582
|
+
...(options.thread ? { thread: options.thread } : {}),
|
|
583
|
+
}, {
|
|
584
|
+
timeoutMs: 0,
|
|
585
|
+
isDone: agent_1.isAgentResult,
|
|
586
|
+
onFrame: (update) => {
|
|
587
|
+
if (update.type === "agent.status")
|
|
588
|
+
options.onStatus?.({ label: update.payload.label, state: update.payload.state });
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
if (!(0, agent_1.isAgentResult)(frame))
|
|
592
|
+
throw new protocol_1.LinkError("bad_frame", `Expected an agent.result, got "${frame.type}".`);
|
|
593
|
+
const { payload } = frame;
|
|
594
|
+
if (!payload.ok)
|
|
595
|
+
throw new protocol_1.LinkError(payload.code, payload.error);
|
|
596
|
+
return { text: payload.output, thread: payload.thread };
|
|
597
|
+
}
|
|
522
598
|
async registerTools(tools) {
|
|
523
599
|
if (!tools.length)
|
|
524
600
|
return;
|
|
@@ -725,7 +801,7 @@ class Link {
|
|
|
725
801
|
}
|
|
726
802
|
handleToolCall(frame) {
|
|
727
803
|
const { callId, localId, args, meta } = frame.payload;
|
|
728
|
-
const tool = this.
|
|
804
|
+
const tool = this.findTool(localId);
|
|
729
805
|
if (!tool) {
|
|
730
806
|
this.send("tool.result", { ok: false, error: `This link has no tool "${localId}".` }, frame.id);
|
|
731
807
|
return;
|
package/dist/link/protocol.d.ts
CHANGED
|
@@ -24,6 +24,22 @@ export type LinkToolDescriptor = {
|
|
|
24
24
|
platforms?: string[];
|
|
25
25
|
timeoutMs?: number;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* An agent as declared to the server: a prompt, a model and the tools it works with.
|
|
29
|
+
*
|
|
30
|
+
* The tools travel inside the agent because that is what places them: reachable through the
|
|
31
|
+
* agent only, never from a chat directly.
|
|
32
|
+
*/
|
|
33
|
+
export type LinkAgentDescriptor = {
|
|
34
|
+
localId: string;
|
|
35
|
+
name: string;
|
|
36
|
+
description: string;
|
|
37
|
+
prompt: string;
|
|
38
|
+
model?: string;
|
|
39
|
+
tools: Omit<LinkToolDescriptor, "platforms">[];
|
|
40
|
+
display?: LinkToolDescriptor["display"];
|
|
41
|
+
defaultEnabled?: boolean;
|
|
42
|
+
};
|
|
27
43
|
export type LinkHookEventDeclaration = {
|
|
28
44
|
name: string;
|
|
29
45
|
description?: string;
|
|
@@ -139,6 +155,18 @@ export type LinkClientPayloads = {
|
|
|
139
155
|
chatId: string;
|
|
140
156
|
message: string;
|
|
141
157
|
};
|
|
158
|
+
"agent.register": {
|
|
159
|
+
agents: LinkAgentDescriptor[];
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Talks to one of this link's agents directly. `thread` names the exchange the message
|
|
163
|
+
* continues; the same thread carries the same memory.
|
|
164
|
+
*/
|
|
165
|
+
"agent.chat": {
|
|
166
|
+
localId: string;
|
|
167
|
+
message: string;
|
|
168
|
+
thread?: string;
|
|
169
|
+
};
|
|
142
170
|
};
|
|
143
171
|
export type LinkClientFrameType = keyof LinkClientPayloads;
|
|
144
172
|
/**
|
|
@@ -230,6 +258,25 @@ export type LinkServerPayloads = {
|
|
|
230
258
|
reason: string;
|
|
231
259
|
reconnectAfterMs: number;
|
|
232
260
|
};
|
|
261
|
+
/** Progress on an `agent.chat`, in the words the agent's status feed would show a conversation. */
|
|
262
|
+
"agent.status": {
|
|
263
|
+
localId: string;
|
|
264
|
+
label: string;
|
|
265
|
+
state: "running" | "completed" | "failed";
|
|
266
|
+
};
|
|
267
|
+
/** The agent's reply, exactly once per `agent.chat`. */
|
|
268
|
+
"agent.result": {
|
|
269
|
+
localId: string;
|
|
270
|
+
thread: string;
|
|
271
|
+
ok: true;
|
|
272
|
+
output: string;
|
|
273
|
+
} | {
|
|
274
|
+
localId: string;
|
|
275
|
+
thread?: string;
|
|
276
|
+
ok: false;
|
|
277
|
+
code: string;
|
|
278
|
+
error: string;
|
|
279
|
+
};
|
|
233
280
|
/**
|
|
234
281
|
* The full set this connection should watch, for the sources it has registered.
|
|
235
282
|
*
|
|
@@ -207,6 +207,16 @@ export type ResponseStatusPayload = {
|
|
|
207
207
|
/** When the stop was requested. */
|
|
208
208
|
at: number;
|
|
209
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* Messages steered into the turn that never reached the model, in the order they were sent.
|
|
212
|
+
*
|
|
213
|
+
* Steering only lands at a step boundary, and a turn does not always have another one: it
|
|
214
|
+
* stops, it fails, or the model answers without needing a further step. The text is not in
|
|
215
|
+
* the conversation's history — it was never said to anyone — so it is handed back here and
|
|
216
|
+
* the client decides what to do with it. Send it as an ordinary message when the turn
|
|
217
|
+
* merely ended; put it back in front of the user when they are the one who stopped it.
|
|
218
|
+
*/
|
|
219
|
+
undeliveredSteer?: string[];
|
|
210
220
|
/** Rich response metadata — only present on the final `completed: true` event.
|
|
211
221
|
* Populated by the caller (e.g. gateway) after the response finishes and usage is available. */
|
|
212
222
|
metadata?: ResponseMetadata;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -212,6 +212,45 @@ whatever tier and permission gating it carries, which tools bolted onto the chat
|
|
|
212
212
|
An unknown platform is rejected when you register, not ignored: a tool reachable from nowhere looks
|
|
213
213
|
exactly like a tool that is broken.
|
|
214
214
|
|
|
215
|
+
### Agents: your tools behind a worker of your own
|
|
216
|
+
|
|
217
|
+
When your client has more tools than a chat should see, or tools that only make sense together,
|
|
218
|
+
declare an agent. It lives on the server for as long as the link does, works with tools that run
|
|
219
|
+
here, and to Alfred it is one tool:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
import { Agent, Tool } from "@butlerbot/sdk";
|
|
223
|
+
|
|
224
|
+
const grind = new Tool({ id: "grind", description: "Grind beans.", run: async () => grinder.run() });
|
|
225
|
+
const brew = new Tool({ id: "brew", description: "Brew from the ground beans.", run: async () => machine.brew() });
|
|
226
|
+
|
|
227
|
+
const barista = new Agent({
|
|
228
|
+
id: "barista",
|
|
229
|
+
name: "Barista",
|
|
230
|
+
description: "Runs the kitchen coffee machine: grinding, brewing, cleaning.",
|
|
231
|
+
prompt: "You operate a coffee machine. Always grind before you brew. Report what you made.",
|
|
232
|
+
model: "DeepSeek-V4-Flash",
|
|
233
|
+
tools: [grind, brew],
|
|
234
|
+
});
|
|
235
|
+
link.addAgent(barista);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The tools come with the agent — no `addTool` for them, and Alfred never sees them directly. The
|
|
239
|
+
prompt is where a thousand tools become one coherent worker.
|
|
240
|
+
|
|
241
|
+
You can talk to the agent yourself. The run happens on the server, on your account; the question
|
|
242
|
+
and the answer live here:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const { text } = await barista.chat("Make me a flat white.");
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
One `Agent` remembers across `chat` calls; pass `{ thread }` to name the conversation yourself, or
|
|
249
|
+
`newThread()` to start over. This is what makes a hook callback useful on its own: something
|
|
250
|
+
happens, your code notices, and you ask an agent what to do about it.
|
|
251
|
+
|
|
252
|
+
Needs `link.tools.register` to declare agents, and `tools.run` to talk to one directly.
|
|
253
|
+
|
|
215
254
|
### Tools belong to the user, not to a conversation
|
|
216
255
|
|
|
217
256
|
Once a tool is registered, Alfred can call it anywhere that user talks to it — the web
|