@agentlinkdev/agentlink 0.1.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/src/channel.ts ADDED
@@ -0,0 +1,91 @@
1
+ import type { AgentLinkConfig, MessageEnvelope } from "./types.js";
2
+ import { createEnvelope, TOPICS } from "./types.js";
3
+ import type { StateManager } from "./state.js";
4
+ import type { ContactsManager } from "./contacts.js";
5
+ import type { MqttService } from "./mqtt-service.js";
6
+ import type { Logger } from "./mqtt-client.js";
7
+
8
+ export interface ChannelPlugin {
9
+ id: string;
10
+ meta: {
11
+ id: string;
12
+ label: string;
13
+ selectionLabel: string;
14
+ blurb: string;
15
+ aliases: string[];
16
+ };
17
+ capabilities: {
18
+ chatTypes: string[];
19
+ };
20
+ config: {
21
+ listAccountIds: () => string[];
22
+ resolveAccount: () => { accountId: string; enabled: boolean; configured: boolean };
23
+ };
24
+ outbound: {
25
+ deliveryMode: string;
26
+ sendText: (params: {
27
+ text: string;
28
+ threadId?: string;
29
+ channelId?: string;
30
+ }) => Promise<{ ok: boolean; error?: string }>;
31
+ };
32
+ }
33
+
34
+ export function createChannelPlugin(
35
+ config: AgentLinkConfig,
36
+ state: StateManager,
37
+ contacts: ContactsManager,
38
+ mqtt: MqttService,
39
+ logger: Logger,
40
+ ): ChannelPlugin {
41
+ return {
42
+ id: "agentlink",
43
+ meta: {
44
+ id: "agentlink",
45
+ label: "AgentLink",
46
+ selectionLabel: "AgentLink (Agent Coordination)",
47
+ blurb: "Agent-to-agent coordination channel",
48
+ aliases: ["al"],
49
+ },
50
+ capabilities: {
51
+ chatTypes: ["direct", "group"],
52
+ },
53
+ config: {
54
+ listAccountIds: () => [config.agent.id],
55
+ resolveAccount: () => ({
56
+ accountId: config.agent.id,
57
+ enabled: true,
58
+ configured: true,
59
+ }),
60
+ },
61
+ outbound: {
62
+ deliveryMode: "direct",
63
+ async sendText({ text, threadId, channelId }) {
64
+ if (!threadId) return { ok: false, error: "No group context" };
65
+
66
+ const group = state.getGroup(threadId);
67
+ if (!group) return { ok: false, error: "Group not found" };
68
+
69
+ const envelope = createEnvelope(config.agent.id, {
70
+ group_id: threadId,
71
+ intent_id: group.intent_id,
72
+ to: channelId ?? "group",
73
+ type: "chat",
74
+ payload: { text },
75
+ });
76
+
77
+ await mqtt.publishEnvelope(
78
+ TOPICS.groupMessages(threadId, config.agent.id),
79
+ envelope,
80
+ );
81
+
82
+ // Track idle turns for anti-deadlock (chat without job/proposal increments)
83
+ if (group.driver === config.agent.id) {
84
+ state.incrementIdleTurns(threadId);
85
+ }
86
+
87
+ return { ok: true };
88
+ },
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,69 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export interface ContactEntry {
5
+ agent_id: string;
6
+ added: string;
7
+ }
8
+
9
+ export interface ContactsManager {
10
+ resolve(nameOrId: string): string | null;
11
+ add(name: string, agentId: string): void;
12
+ remove(name: string): void;
13
+ has(name: string): boolean;
14
+ getAll(): Record<string, ContactEntry>;
15
+ getNameByAgentId(agentId: string): string | null;
16
+ }
17
+
18
+ export function createContacts(dataDir: string): ContactsManager {
19
+ const filePath = path.join(dataDir, "contacts.json");
20
+ let contacts: Record<string, ContactEntry> = {};
21
+
22
+ if (fs.existsSync(filePath)) {
23
+ const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
24
+ contacts = raw.contacts ?? {};
25
+ }
26
+
27
+ function save() {
28
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
29
+ fs.writeFileSync(filePath, JSON.stringify({ contacts }, null, 2));
30
+ }
31
+
32
+ return {
33
+ resolve(nameOrId) {
34
+ if (contacts[nameOrId]) return contacts[nameOrId].agent_id;
35
+ for (const entry of Object.values(contacts)) {
36
+ if (entry.agent_id === nameOrId) return nameOrId;
37
+ }
38
+ return null;
39
+ },
40
+
41
+ add(name, agentId) {
42
+ contacts[name] = {
43
+ agent_id: agentId,
44
+ added: new Date().toISOString().split("T")[0],
45
+ };
46
+ save();
47
+ },
48
+
49
+ remove(name) {
50
+ delete contacts[name];
51
+ save();
52
+ },
53
+
54
+ has(name) {
55
+ return name in contacts;
56
+ },
57
+
58
+ getAll() {
59
+ return { ...contacts };
60
+ },
61
+
62
+ getNameByAgentId(agentId) {
63
+ for (const [name, entry] of Object.entries(contacts)) {
64
+ if (entry.agent_id === agentId) return name;
65
+ }
66
+ return null;
67
+ },
68
+ };
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,269 @@
1
+ import type { AgentLinkConfig } from "./types.js";
2
+ import { resolveConfig } from "./types.js";
3
+ import { createContacts } from "./contacts.js";
4
+ import { createState } from "./state.js";
5
+ import { createMqttService } from "./mqtt-service.js";
6
+ import { createInviteManager } from "./invite.js";
7
+ import { createJobManager } from "./jobs.js";
8
+ import { createTools } from "./tools.js";
9
+ import { createChannelPlugin } from "./channel.js";
10
+ import { shouldProcess } from "./routing.js";
11
+ import type { Logger } from "./mqtt-client.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Minimal OC Plugin API type (matches what we use from OpenClaw's PluginApi)
15
+ // ---------------------------------------------------------------------------
16
+
17
+ interface PluginApi {
18
+ config: Record<string, unknown>;
19
+ pluginConfig?: Record<string, unknown>;
20
+ logger: Logger;
21
+ registerService(service: { id: string; start: () => Promise<void>; stop: () => Promise<void> }): void;
22
+ registerTool(tool: {
23
+ name: string;
24
+ label: string;
25
+ description: string;
26
+ parameters: unknown;
27
+ execute: (_id: string, params: Record<string, unknown>) => Promise<{
28
+ content: Array<{ type: "text"; text: string }>;
29
+ details: unknown;
30
+ }>;
31
+ }): void;
32
+ registerChannel?(registration: { plugin: unknown }): void;
33
+ registerCli?(registrar: (ctx: { program: unknown }) => void, opts?: { commands?: string[] }): void;
34
+ on?(hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }): void;
35
+ runtime?: {
36
+ executeTool(params: {
37
+ toolName: string;
38
+ params: Record<string, unknown>;
39
+ ctx?: unknown;
40
+ }): Promise<unknown>;
41
+ };
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Plugin definition object (preferred OC pattern)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export default {
49
+ id: "agentlink",
50
+ name: "AgentLink",
51
+ description: "Agent-to-agent coordination over MQTT",
52
+ register,
53
+ };
54
+
55
+ function register(api: PluginApi) {
56
+ const config = resolveConfig(api.pluginConfig ?? {});
57
+ const contacts = createContacts(config.dataDir);
58
+ const state = createState(config.dataDir);
59
+ const logger = api.logger;
60
+
61
+ function log(msg: string) {
62
+ if (config.outputMode === "debug") {
63
+ logger.info(`[AgentLink] ${msg}`);
64
+ }
65
+ }
66
+
67
+ const mqttService = createMqttService(config, state, contacts, logger);
68
+
69
+ const invites = createInviteManager(config, mqttService);
70
+
71
+ // executeTool bridge: calls the local OC tool when a job_request arrives
72
+ const executeTool = api.runtime
73
+ ? async (toolId: string, input: string): Promise<string> => {
74
+ const result = await api.runtime!.executeTool({
75
+ toolName: toolId,
76
+ params: { input },
77
+ });
78
+ return typeof result === "string" ? result : JSON.stringify(result);
79
+ }
80
+ : undefined;
81
+
82
+ const jobs = createJobManager(config, state, mqttService, logger, executeTool);
83
+
84
+ // Wire inbound message handling
85
+ mqttService.onGroupMessage((msg) => {
86
+ // Check if this agent should process the message
87
+ if (!shouldProcess(msg, config.agent.id, config.agent.capabilities)) {
88
+ return;
89
+ }
90
+
91
+ if (msg.type === "job_request") {
92
+ jobs.handleJobRequest(msg);
93
+ return;
94
+ }
95
+
96
+ if (msg.type === "job_response" && msg.correlation_id && state.hasPendingJob(msg.correlation_id)) {
97
+ jobs.handleJobResponse(msg);
98
+ return;
99
+ }
100
+
101
+ // General coordination message — log it
102
+ const senderName = contacts.getNameByAgentId(msg.from) ?? msg.from;
103
+ log(`Message from ${senderName}: ${msg.payload.text ?? "(no text)"}`);
104
+ });
105
+
106
+ mqttService.onInboxMessage((_topic, raw) => {
107
+ const msg = raw as Record<string, unknown>;
108
+ if (msg.type === "invite") {
109
+ log(`Invite received from ${msg.from}: "${msg.goal}"`);
110
+ // For V1: auto-join if from known contact
111
+ const fromId = msg.from as string;
112
+ if (contacts.resolve(fromId)) {
113
+ log(`Auto-joining (known contact: ${contacts.getNameByAgentId(fromId) ?? fromId})`);
114
+ // Auto-join logic handled by the agent's LLM calling agentlink_join_group
115
+ // or we could auto-accept here for known contacts
116
+ }
117
+ }
118
+ });
119
+
120
+ mqttService.onSystemEvent((raw) => {
121
+ const msg = raw as Record<string, unknown>;
122
+ if (msg && typeof msg === "object" && "type" in msg) {
123
+ const envelope = msg as Record<string, unknown>;
124
+ if (envelope.type === "join") {
125
+ const from = envelope.from as string;
126
+ const groupId = envelope.group_id as string;
127
+ const group = state.getGroup(groupId);
128
+ if (group && !group.participants.includes(from)) {
129
+ group.participants.push(from);
130
+ state.updateGroup(groupId, { participants: group.participants });
131
+ }
132
+ log(`${contacts.getNameByAgentId(from) ?? from} joined group ${groupId}`);
133
+ }
134
+ if (envelope.type === "leave") {
135
+ const groupId = envelope.group_id as string;
136
+ // If this is a completion message from the driver, clean up
137
+ const group = state.getGroup(groupId);
138
+ if (group && envelope.from !== config.agent.id) {
139
+ mqttService.unsubscribeGroup(groupId);
140
+ state.removeGroup(groupId);
141
+ log(`Group ${groupId} closed by driver`);
142
+ }
143
+ }
144
+ }
145
+ });
146
+
147
+ // Background MQTT connection
148
+ api.registerService({
149
+ id: "agentlink-mqtt",
150
+ start: async () => {
151
+ await mqttService.start();
152
+
153
+ // Process pending joins from CLI --join flag
154
+ const pendingJoins = state.getPendingJoins();
155
+ for (const code of pendingJoins) {
156
+ try {
157
+ log(`Processing pending join: ${code}`);
158
+ const invite = await invites.resolveInviteCode(code);
159
+ if (invite) {
160
+ const groupId = invite.group_id;
161
+ await mqttService.subscribeGroup(groupId);
162
+ state.addGroup({
163
+ group_id: groupId,
164
+ driver: invite.from,
165
+ goal: invite.goal,
166
+ done_when: "",
167
+ intent_id: "",
168
+ participants: [invite.from],
169
+ status: "active",
170
+ idle_turns: 0,
171
+ created_at: new Date().toISOString(),
172
+ });
173
+ if (!contacts.resolve(invite.from)) {
174
+ contacts.add(invite.from, invite.from);
175
+ }
176
+ state.removePendingJoin(code);
177
+ log(`Auto-joined group from setup: ${code}`);
178
+ } else {
179
+ log(`Pending join ${code}: invite not found (will retry next restart)`);
180
+ }
181
+ } catch (err) {
182
+ const msg = err instanceof Error ? err.message : String(err);
183
+ log(`Failed to auto-join ${code}: ${msg}`);
184
+ }
185
+ }
186
+ },
187
+ stop: () => mqttService.stop(),
188
+ });
189
+
190
+ // Register the 5 agent tools
191
+ const tools = createTools(config, state, contacts, mqttService, invites, jobs, logger);
192
+ for (const tool of tools) {
193
+ api.registerTool(tool);
194
+ }
195
+
196
+ // Optional channel registration
197
+ if (api.registerChannel) {
198
+ const channelPlugin = createChannelPlugin(config, state, contacts, mqttService, logger);
199
+ api.registerChannel({ plugin: channelPlugin });
200
+ }
201
+
202
+ // Anti-deadlock system prompt injection
203
+ if (api.on) {
204
+ api.on("before_prompt_build", () => {
205
+ const activeGroups = state.getActiveGroups();
206
+ if (activeGroups.length === 0) return {};
207
+
208
+ const driverGroups = activeGroups
209
+ .map((id) => state.getGroup(id))
210
+ .filter((g) => g?.driver === config.agent.id);
211
+
212
+ if (driverGroups.length === 0) return {};
213
+
214
+ let prompt = "\n\n## AgentLink Coordination Rules (MANDATORY)\n";
215
+ prompt += "You are the DRIVER of active coordination(s). You MUST follow these rules:\n";
216
+ prompt += "1. Issue direct jobs (agentlink_submit_job) instead of asking open-ended questions.\n";
217
+ prompt += "2. Make concrete proposals with specifics (time, place, price), not vague suggestions.\n";
218
+ prompt += "3. After 3 turns of discussion without a job, proposal, or completion, you MUST force progress.\n";
219
+ prompt += "4. Declare completion (agentlink_complete) as soon as the done_when condition is met.\n";
220
+ prompt += "5. Hub-and-spoke: you mediate all coordination. Participants respond to you, not each other.\n\n";
221
+
222
+ for (const group of driverGroups) {
223
+ if (!group) continue;
224
+ prompt += `Active: "${group.goal}" | Done when: "${group.done_when}" | Idle turns: ${group.idle_turns}/3\n`;
225
+ if (group.idle_turns >= 3) {
226
+ prompt += ` WARNING: 3 idle turns reached. You MUST take concrete action NOW.\n`;
227
+ }
228
+ }
229
+
230
+ return { appendSystemContext: prompt };
231
+ });
232
+ }
233
+
234
+ // CLI commands
235
+ if (api.registerCli) {
236
+ api.registerCli(
237
+ ({ program }: { program: any }) => {
238
+ const cmd = program.command("agentlink").description("AgentLink agent coordination");
239
+ cmd
240
+ .command("status")
241
+ .description("Show AgentLink connection and group status")
242
+ .action(() => {
243
+ console.log(`Agent ID: ${config.agent.id}`);
244
+ console.log(`Broker: ${config.brokerUrl}`);
245
+ console.log(`Connected: ${mqttService.getClient().isConnected()}`);
246
+ console.log(`Active groups: ${state.getActiveGroups().length}`);
247
+ console.log(
248
+ `Capabilities: ${config.agent.capabilities.map((c) => c.name).join(", ") || "none"}`,
249
+ );
250
+ });
251
+ cmd
252
+ .command("contacts")
253
+ .description("List known contacts")
254
+ .action(() => {
255
+ const all = contacts.getAll();
256
+ const entries = Object.entries(all);
257
+ if (entries.length === 0) {
258
+ console.log("No contacts.");
259
+ return;
260
+ }
261
+ for (const [name, entry] of entries) {
262
+ console.log(`${name} -> ${entry.agent_id} (added ${entry.added})`);
263
+ }
264
+ });
265
+ },
266
+ { commands: ["agentlink"] },
267
+ );
268
+ }
269
+ }
package/src/invite.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { v4 as uuid } from "uuid";
2
+ import type { AgentLinkConfig, InviteMessage, InviteCodePayload } from "./types.js";
3
+ import { TOPICS } from "./types.js";
4
+ import type { MqttService } from "./mqtt-service.js";
5
+
6
+ export interface InviteCodeResult {
7
+ code: string;
8
+ shareableMessage: string;
9
+ }
10
+
11
+ export interface InviteManager {
12
+ createInviteCode(groupId: string, from: string, goal: string): Promise<InviteCodeResult>;
13
+ resolveInviteCode(code: string): Promise<InviteCodePayload | null>;
14
+ sendDirectInvite(targetAgentId: string, groupId: string, goal: string, doneWhen: string): Promise<void>;
15
+ }
16
+
17
+ export function createInviteManager(
18
+ config: AgentLinkConfig,
19
+ mqtt: MqttService,
20
+ ): InviteManager {
21
+ return {
22
+ async createInviteCode(groupId, from, goal) {
23
+ const code = uuid().replace(/-/g, "").substring(0, 6).toUpperCase();
24
+
25
+ const payload: InviteCodePayload = {
26
+ group_id: groupId,
27
+ from,
28
+ goal,
29
+ created_at: new Date().toISOString(),
30
+ };
31
+
32
+ await mqtt.publish(
33
+ TOPICS.inviteCode(code),
34
+ JSON.stringify(payload),
35
+ { retain: true, qos: 1 },
36
+ );
37
+
38
+ const shareableMessage = [
39
+ `Join my agent coordination: ${code}`,
40
+ `1. Install AgentLink: openclaw plugins install @agentlinkdev/openclaw`,
41
+ `2. Tell your agent: "Join AgentLink group ${code}"`,
42
+ ].join("\n");
43
+
44
+ return { code, shareableMessage };
45
+ },
46
+
47
+ async resolveInviteCode(code) {
48
+ return new Promise((resolve) => {
49
+ const topic = TOPICS.inviteCode(code);
50
+ let resolved = false;
51
+
52
+ const timeout = setTimeout(() => {
53
+ if (!resolved) {
54
+ resolved = true;
55
+ resolve(null);
56
+ }
57
+ }, 5000);
58
+
59
+ const handler = (msgTopic: string, payload: Buffer) => {
60
+ if (msgTopic === topic && !resolved) {
61
+ resolved = true;
62
+ clearTimeout(timeout);
63
+ try {
64
+ resolve(JSON.parse(payload.toString()));
65
+ } catch {
66
+ resolve(null);
67
+ }
68
+ }
69
+ };
70
+
71
+ mqtt.getClient().onMessage(handler);
72
+ mqtt.getClient().subscribe(topic).catch(() => {
73
+ if (!resolved) {
74
+ resolved = true;
75
+ clearTimeout(timeout);
76
+ resolve(null);
77
+ }
78
+ });
79
+ });
80
+ },
81
+
82
+ async sendDirectInvite(targetAgentId, groupId, goal, doneWhen) {
83
+ const invite: InviteMessage = {
84
+ type: "invite",
85
+ group_id: groupId,
86
+ from: config.agent.id,
87
+ goal,
88
+ done_when: doneWhen,
89
+ ts: new Date().toISOString(),
90
+ };
91
+
92
+ await mqtt.publish(
93
+ TOPICS.inbox(targetAgentId),
94
+ JSON.stringify(invite),
95
+ { qos: 1 },
96
+ );
97
+ },
98
+ };
99
+ }
package/src/jobs.ts ADDED
@@ -0,0 +1,156 @@
1
+ import { v4 as uuid } from "uuid";
2
+ import type { AgentLinkConfig, MessageEnvelope } from "./types.js";
3
+ import { createEnvelope, TOPICS } from "./types.js";
4
+ import type { StateManager } from "./state.js";
5
+ import type { MqttService } from "./mqtt-service.js";
6
+ import type { Logger } from "./mqtt-client.js";
7
+
8
+ export interface SubmitJobParams {
9
+ groupId: string;
10
+ intentId: string;
11
+ targetAgentId?: string;
12
+ capability: string;
13
+ text: string;
14
+ }
15
+
16
+ export interface JobManager {
17
+ submitJob(params: SubmitJobParams): Promise<string>;
18
+ handleJobResponse(msg: MessageEnvelope): void;
19
+ handleJobRequest(msg: MessageEnvelope): Promise<MessageEnvelope | null>;
20
+ }
21
+
22
+ export function createJobManager(
23
+ config: AgentLinkConfig,
24
+ state: StateManager,
25
+ mqtt: MqttService,
26
+ logger: Logger,
27
+ executeTool?: (toolId: string, input: string) => Promise<string>,
28
+ ): JobManager {
29
+ return {
30
+ async submitJob(params) {
31
+ const correlationId = uuid();
32
+ const envelope = createEnvelope(config.agent.id, {
33
+ group_id: params.groupId,
34
+ intent_id: params.intentId,
35
+ to: params.targetAgentId ?? "group",
36
+ type: "job_request",
37
+ correlation_id: correlationId,
38
+ payload: {
39
+ text: params.text,
40
+ capability: params.capability,
41
+ },
42
+ });
43
+
44
+ state.addJob({
45
+ correlation_id: correlationId,
46
+ group_id: params.groupId,
47
+ target: params.targetAgentId ?? "group",
48
+ capability: params.capability,
49
+ status: "requested",
50
+ sent_at: envelope.ts,
51
+ text: params.text,
52
+ });
53
+
54
+ await mqtt.publishEnvelope(
55
+ TOPICS.groupMessages(params.groupId, config.agent.id),
56
+ envelope,
57
+ );
58
+
59
+ // Start timeout timer
60
+ setTimeout(() => {
61
+ if (state.hasPendingJob(correlationId)) {
62
+ state.completeJob(correlationId, "failed");
63
+ logger.info(`[AgentLink] Job ${correlationId} timed out (${params.capability})`);
64
+ }
65
+ }, config.jobTimeoutMs);
66
+
67
+ return correlationId;
68
+ },
69
+
70
+ handleJobResponse(msg) {
71
+ if (!msg.correlation_id) return;
72
+ const job = state.getJob(msg.correlation_id);
73
+ if (!job) return;
74
+
75
+ const status = msg.payload.status ?? "completed";
76
+ state.completeJob(msg.correlation_id, status);
77
+ logger.info(
78
+ `[AgentLink] Job ${msg.correlation_id} ${status}: ${msg.payload.result ?? "(no result)"}`,
79
+ );
80
+ },
81
+
82
+ async handleJobRequest(msg) {
83
+ const capability = msg.payload.capability;
84
+ if (!capability) return null;
85
+
86
+ const cap = config.agent.capabilities.find((c) => c.name === capability);
87
+ if (!cap) {
88
+ const response = createEnvelope(config.agent.id, {
89
+ group_id: msg.group_id,
90
+ intent_id: msg.intent_id,
91
+ to: msg.from,
92
+ type: "job_response",
93
+ correlation_id: msg.correlation_id,
94
+ payload: {
95
+ status: "failed",
96
+ result: `Capability '${capability}' not available`,
97
+ capability,
98
+ },
99
+ });
100
+ await mqtt.publishEnvelope(
101
+ TOPICS.groupMessages(msg.group_id, config.agent.id),
102
+ response,
103
+ );
104
+ return response;
105
+ }
106
+
107
+ logger.info(`[AgentLink] Running local tool: ${cap.tool} for capability: ${capability}`);
108
+
109
+ let result: string;
110
+ try {
111
+ if (executeTool) {
112
+ result = await executeTool(cap.tool, msg.payload.text ?? "");
113
+ } else {
114
+ result = `Tool execution not available (no executeTool provided)`;
115
+ }
116
+ } catch (err: unknown) {
117
+ const errMsg = err instanceof Error ? err.message : String(err);
118
+ const response = createEnvelope(config.agent.id, {
119
+ group_id: msg.group_id,
120
+ intent_id: msg.intent_id,
121
+ to: msg.from,
122
+ type: "job_response",
123
+ correlation_id: msg.correlation_id,
124
+ payload: {
125
+ status: "failed",
126
+ result: `Tool execution error: ${errMsg}`,
127
+ capability,
128
+ },
129
+ });
130
+ await mqtt.publishEnvelope(
131
+ TOPICS.groupMessages(msg.group_id, config.agent.id),
132
+ response,
133
+ );
134
+ return response;
135
+ }
136
+
137
+ const response = createEnvelope(config.agent.id, {
138
+ group_id: msg.group_id,
139
+ intent_id: msg.intent_id,
140
+ to: msg.from,
141
+ type: "job_response",
142
+ correlation_id: msg.correlation_id,
143
+ payload: {
144
+ status: "completed",
145
+ result,
146
+ capability,
147
+ },
148
+ });
149
+ await mqtt.publishEnvelope(
150
+ TOPICS.groupMessages(msg.group_id, config.agent.id),
151
+ response,
152
+ );
153
+ return response;
154
+ },
155
+ };
156
+ }