Compare commits
6 Commits
86e8d2ac8d
...
feat/triag
| Author | SHA1 | Date | |
|---|---|---|---|
| c5fd091f5a | |||
| c5a33c33ec | |||
| 28f5083679 | |||
| a060ff98a2 | |||
| b9a5456d57 | |||
| d1d5ad10ca |
@@ -14,12 +14,7 @@
|
|||||||
"create-work-channel",
|
"create-work-channel",
|
||||||
"create-report-channel",
|
"create-report-channel",
|
||||||
"create-discussion-channel",
|
"create-discussion-channel",
|
||||||
"discussion-complete",
|
"discussion-complete"
|
||||||
"fabric-canvas",
|
|
||||||
"fabric-channel",
|
|
||||||
"fabric-send-message",
|
|
||||||
"fabric-channel-list",
|
|
||||||
"fabric-message-history"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"configSchema": {
|
"configSchema": {
|
||||||
|
|||||||
@@ -190,76 +190,6 @@ export class FabricClient {
|
|||||||
removeCanvas(endpoint: string, token: string, channelId: string): Promise<unknown> {
|
removeCanvas(endpoint: string, token: string, channelId: string): Promise<unknown> {
|
||||||
return this.req('DELETE', this.canvasUrl(endpoint, channelId), token);
|
return this.req('DELETE', this.canvasUrl(endpoint, channelId), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- channel discovery + message read (used by the agent-facing
|
|
||||||
// fabric-channel-list / fabric-message-history tools) ----
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List channels in a guild visible to the calling user. Backend
|
|
||||||
* filters to public + channels the user is a member of.
|
|
||||||
*/
|
|
||||||
listChannels(
|
|
||||||
guildEndpoint: string,
|
|
||||||
guildToken: string,
|
|
||||||
guildNodeId: string,
|
|
||||||
): Promise<Array<{
|
|
||||||
id: string;
|
|
||||||
guildId: string;
|
|
||||||
name: string;
|
|
||||||
xType: string;
|
|
||||||
kind: string;
|
|
||||||
isPublic: boolean;
|
|
||||||
closed: boolean;
|
|
||||||
lastSeq: number;
|
|
||||||
createdAt: string;
|
|
||||||
}>> {
|
|
||||||
return this.req(
|
|
||||||
'GET',
|
|
||||||
`${guildEndpoint}/api/channels?guildId=${encodeURIComponent(guildNodeId)}`,
|
|
||||||
guildToken,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Page through a channel's message history by `seq`.
|
|
||||||
*
|
|
||||||
* Backend defaults: 50 / call, max 200. The `seq` field starts at 1
|
|
||||||
* per channel; pass `seqFrom=channel.lastSeq - N + 1` to get the
|
|
||||||
* tail. Page metadata in the response describes what to ask next.
|
|
||||||
*/
|
|
||||||
listMessages(
|
|
||||||
guildEndpoint: string,
|
|
||||||
guildToken: string,
|
|
||||||
channelId: string,
|
|
||||||
opts: { seqFrom?: number; seqTo?: number; limit?: number } = {},
|
|
||||||
): Promise<{
|
|
||||||
items: Array<{
|
|
||||||
messageId: string;
|
|
||||||
seq: number;
|
|
||||||
content: string;
|
|
||||||
authorUserId: string;
|
|
||||||
createdAt: string;
|
|
||||||
editedAt: string | null;
|
|
||||||
deletedAt: string | null;
|
|
||||||
isDeleted: boolean;
|
|
||||||
}>;
|
|
||||||
page: {
|
|
||||||
seqFrom: number;
|
|
||||||
seqTo: number;
|
|
||||||
limit: number;
|
|
||||||
returned: number;
|
|
||||||
hasMore: boolean;
|
|
||||||
nextExpectedSeq: number;
|
|
||||||
highestCommittedSeq: number;
|
|
||||||
};
|
|
||||||
}> {
|
|
||||||
const qs = new URLSearchParams();
|
|
||||||
if (opts.seqFrom !== undefined) qs.set('seq_from', String(opts.seqFrom));
|
|
||||||
if (opts.seqTo !== undefined) qs.set('seq_to', String(opts.seqTo));
|
|
||||||
if (opts.limit !== undefined) qs.set('limit', String(opts.limit));
|
|
||||||
const url = `${guildEndpoint}/api/channels/${channelId}/messages` + (qs.toString() ? `?${qs}` : '');
|
|
||||||
return this.req('GET', url, guildToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CanvasFormat = 'md' | 'html' | 'text';
|
export type CanvasFormat = 'md' | 'html' | 'text';
|
||||||
|
|||||||
169
src/tools.ts
169
src/tools.ts
@@ -243,173 +243,4 @@ export function registerFabricTools(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// fabric-send-message: post a message into a specific channel.
|
|
||||||
//
|
|
||||||
// Unlike a normal channel reply (which goes back to whatever channel
|
|
||||||
// woke the agent), this lets the agent proactively initiate text into
|
|
||||||
// any channel they are a member of — e.g. ARD broadcasting daily
|
|
||||||
// workload to #agents-room, or triage agent following up on an
|
|
||||||
// already-routed task by commenting in #updates.
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
api.registerTool((ctx: Ctx) => ({
|
|
||||||
name: 'fabric-send-message',
|
|
||||||
description:
|
|
||||||
'Send a text message into a specific Fabric channel. Author is the calling agent. ' +
|
|
||||||
'Requires guildNodeId + channelId + content. Returns {ok, messageId, seq}.',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['guildNodeId', 'channelId', 'content'],
|
|
||||||
properties: {
|
|
||||||
guildNodeId: { type: 'string' },
|
|
||||||
channelId: { type: 'string' },
|
|
||||||
content: { type: 'string', description: 'Message body (markdown supported by the renderer).' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute: async (p: { guildNodeId: string; channelId: string; content: string }) => {
|
|
||||||
const agentId = ctx.agentId;
|
|
||||||
if (!agentId) return { ok: false, error: 'no agent context' };
|
|
||||||
const { session, guild, token } = await ctxGuild(agentId, p.guildNodeId);
|
|
||||||
const res = (await client.postMessage(
|
|
||||||
guild.endpoint,
|
|
||||||
token,
|
|
||||||
p.channelId,
|
|
||||||
p.content,
|
|
||||||
session.user.id,
|
|
||||||
)) as { messageId?: string; seq?: number };
|
|
||||||
return { ok: true, messageId: res.messageId, seq: res.seq };
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// fabric-channel-list: enumerate channels the calling agent can see
|
|
||||||
// in a given guild. Backend filters to public channels + channels the
|
|
||||||
// agent is a member of. Returns id / name / xType per channel so the
|
|
||||||
// agent can pick a channelId for fabric-send-message etc.
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
api.registerTool((ctx: Ctx) => ({
|
|
||||||
name: 'fabric-channel-list',
|
|
||||||
description:
|
|
||||||
'List channels visible to the calling agent in a guild. Optional ' +
|
|
||||||
'nameFilter does a case-insensitive substring match client-side. ' +
|
|
||||||
'Use this to find a channelId before fabric-send-message / fabric-message-history.',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['guildNodeId'],
|
|
||||||
properties: {
|
|
||||||
guildNodeId: { type: 'string' },
|
|
||||||
nameFilter: { type: 'string', description: 'optional substring match on channel name (case-insensitive)' },
|
|
||||||
xType: {
|
|
||||||
type: 'string',
|
|
||||||
enum: ['general', 'work', 'report', 'discuss', 'triage', 'custom', 'dm'],
|
|
||||||
description: 'optional filter by x_type',
|
|
||||||
},
|
|
||||||
includeClosed: { type: 'boolean', description: 'default false — closed channels filtered out' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute: async (p: {
|
|
||||||
guildNodeId: string;
|
|
||||||
nameFilter?: string;
|
|
||||||
xType?: string;
|
|
||||||
includeClosed?: boolean;
|
|
||||||
}) => {
|
|
||||||
const agentId = ctx.agentId;
|
|
||||||
if (!agentId) return { ok: false, error: 'no agent context' };
|
|
||||||
const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
|
|
||||||
const all = await client.listChannels(guild.endpoint, token, p.guildNodeId);
|
|
||||||
const needle = (p.nameFilter ?? '').toLowerCase();
|
|
||||||
const filtered = all.filter((c) => {
|
|
||||||
if (!p.includeClosed && c.closed) return false;
|
|
||||||
if (p.xType && c.xType !== p.xType) return false;
|
|
||||||
if (needle && !c.name.toLowerCase().includes(needle)) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
count: filtered.length,
|
|
||||||
channels: filtered.map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
xType: c.xType,
|
|
||||||
isPublic: c.isPublic,
|
|
||||||
closed: c.closed,
|
|
||||||
lastSeq: c.lastSeq,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
// fabric-message-history: read a channel's recent message history by
|
|
||||||
// `seq`. Tail-by-default: when `seqFrom`/`seqTo` are omitted, returns
|
|
||||||
// the last `limit` messages (limit defaults to 20, max 200).
|
|
||||||
//
|
|
||||||
// Use cases: catch-up on a channel that was muted while the agent was
|
|
||||||
// gated; verify a previous message went through; lookup recent
|
|
||||||
// duplicates before opening a new task in triage.
|
|
||||||
// -----------------------------------------------------------------
|
|
||||||
api.registerTool((ctx: Ctx) => ({
|
|
||||||
name: 'fabric-message-history',
|
|
||||||
description:
|
|
||||||
"Read a channel's recent message history. Omit seqFrom/seqTo to " +
|
|
||||||
'tail (last `limit` messages, default 20, max 200). Backend ' +
|
|
||||||
'requires the calling agent to be a channel participant.',
|
|
||||||
parameters: {
|
|
||||||
type: 'object',
|
|
||||||
additionalProperties: false,
|
|
||||||
required: ['guildNodeId', 'channelId'],
|
|
||||||
properties: {
|
|
||||||
guildNodeId: { type: 'string' },
|
|
||||||
channelId: { type: 'string' },
|
|
||||||
seqFrom: { type: 'integer', minimum: 1, description: 'inclusive lower bound; default = tail' },
|
|
||||||
seqTo: { type: 'integer', minimum: 1, description: 'inclusive upper bound; default = channel head' },
|
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 200, description: 'default 20' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
execute: async (p: {
|
|
||||||
guildNodeId: string;
|
|
||||||
channelId: string;
|
|
||||||
seqFrom?: number;
|
|
||||||
seqTo?: number;
|
|
||||||
limit?: number;
|
|
||||||
}) => {
|
|
||||||
const agentId = ctx.agentId;
|
|
||||||
if (!agentId) return { ok: false, error: 'no agent context' };
|
|
||||||
const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
|
|
||||||
const limit = p.limit ?? 20;
|
|
||||||
|
|
||||||
// Tail mode: discover channel head via channel listing, then ask
|
|
||||||
// for [head-limit+1, head]. Avoids needing the agent to know seq.
|
|
||||||
let seqFrom = p.seqFrom;
|
|
||||||
let seqTo = p.seqTo;
|
|
||||||
if (seqFrom === undefined && seqTo === undefined) {
|
|
||||||
const channels = await client.listChannels(guild.endpoint, token, p.guildNodeId);
|
|
||||||
const ch = channels.find((c) => c.id === p.channelId);
|
|
||||||
const head = ch?.lastSeq ?? 0;
|
|
||||||
seqFrom = Math.max(1, head - limit + 1);
|
|
||||||
seqTo = head;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await client.listMessages(guild.endpoint, token, p.channelId, {
|
|
||||||
seqFrom,
|
|
||||||
seqTo,
|
|
||||||
limit,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
page: res.page,
|
|
||||||
messages: res.items.map((m) => ({
|
|
||||||
messageId: m.messageId,
|
|
||||||
seq: m.seq,
|
|
||||||
authorUserId: m.authorUserId,
|
|
||||||
content: m.content,
|
|
||||||
createdAt: m.createdAt,
|
|
||||||
isDeleted: m.isDeleted,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user