feat(plugin): fabric-guild-list + fabric-channel-set-purpose tools + purpose on existing tools
Adds two agent-facing tools that close the discoverability loop:
- fabric-guild-list — enumerates guilds the agent belongs to with
name + purpose + status (no api calls beyond the existing agentLogin
response). Optional nameFilter/purposeFilter for narrowing.
- fabric-channel-set-purpose — PATCH /api/channels/:id { purpose }
so agents can backfill or update an existing channel's purpose.
Extends existing tools:
- fabric-channel-list now returns purpose on each row.
- create-{chat,work,report,discussion}-channel accept optional purpose.
FabricClient + FabricSession type changes carry the new field through.
Manifest contracts.tools updated (jiti loader needs both manifest entry
and onStartup activation to register).
Lets workflows that previously needed hardcoded channel ids instead say
'find a guild whose purpose mentions debate, then a channel of x_type
announce whose purpose covers public debate broadcasts.'
This commit is contained in:
116
src/tools.ts
116
src/tools.ts
@@ -46,7 +46,10 @@ export function registerFabricTools(
|
||||
const makeCreate = (kind: 'chat' | 'work' | 'report' | 'discussion') =>
|
||||
api.registerTool((ctx: Ctx) => ({
|
||||
name: `create-${kind}-channel`,
|
||||
description: `Create a Fabric ${kind} channel (x_type=${X_BY_KIND[kind]}).`,
|
||||
description:
|
||||
`Create a Fabric ${kind} channel (x_type=${X_BY_KIND[kind]}). ` +
|
||||
'Optionally pass `purpose` to describe what this channel is for — ' +
|
||||
'agents browse channels by purpose via fabric-channel-list.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -58,6 +61,14 @@ export function registerFabricTools(
|
||||
memberUserIds: { type: 'array', items: { type: 'string' } },
|
||||
onDuty: { type: 'string', description: 'required for triage-like flows (unused for these kinds)' },
|
||||
listeners: { type: 'array', items: { type: 'string' } },
|
||||
purpose: {
|
||||
type: 'string',
|
||||
description:
|
||||
"Free-form description of what this channel is for. Optional but " +
|
||||
'strongly recommended so other agents can find this channel by ' +
|
||||
'intent (via fabric-channel-list). Can be edited later with ' +
|
||||
'fabric-channel-set-purpose.',
|
||||
},
|
||||
},
|
||||
},
|
||||
execute: async (p: {
|
||||
@@ -65,6 +76,7 @@ export function registerFabricTools(
|
||||
name: string;
|
||||
isPublic?: boolean;
|
||||
memberUserIds?: string[];
|
||||
purpose?: string;
|
||||
}) => {
|
||||
const agentId = ctx.agentId;
|
||||
if (!agentId) return { ok: false, error: 'no agent context' };
|
||||
@@ -75,6 +87,7 @@ export function registerFabricTools(
|
||||
xType: X_BY_KIND[kind],
|
||||
isPublic: p.isPublic ?? false,
|
||||
memberUserIds: p.memberUserIds ?? [],
|
||||
...(p.purpose !== undefined ? { purpose: p.purpose } : {}),
|
||||
});
|
||||
return { ok: true, channelId: ch.id };
|
||||
},
|
||||
@@ -337,11 +350,112 @@ export function registerFabricTools(
|
||||
isPublic: c.isPublic,
|
||||
closed: c.closed,
|
||||
lastSeq: c.lastSeq,
|
||||
purpose: c.purpose ?? null,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// fabric-guild-list: enumerate guilds the calling agent belongs to.
|
||||
// Each row carries `purpose` — free-form description of what the
|
||||
// guild is for (admin-set). Use this as the first step when a
|
||||
// workflow says "find the right guild for X" — pick by purpose,
|
||||
// then fabric-channel-list to find the right channel inside it.
|
||||
// -----------------------------------------------------------------
|
||||
api.registerTool((ctx: Ctx) => ({
|
||||
name: 'fabric-guild-list',
|
||||
description:
|
||||
'List guilds the calling agent is a member of. Returns ' +
|
||||
'{nodeId, name, purpose, status} per row. `purpose` is a free-form ' +
|
||||
"description of what each guild is for. Use this BEFORE " +
|
||||
'fabric-channel-list when a workflow asks you to pick the ' +
|
||||
'right guild by intent (no guild ids hardcoded into workflows).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
nameFilter: {
|
||||
type: 'string',
|
||||
description: 'optional case-insensitive substring match on guild name',
|
||||
},
|
||||
purposeFilter: {
|
||||
type: 'string',
|
||||
description:
|
||||
'optional case-insensitive substring match on guild purpose ' +
|
||||
'(e.g. "debate", "announcements")',
|
||||
},
|
||||
},
|
||||
},
|
||||
execute: async (p: { nameFilter?: string; purposeFilter?: string }) => {
|
||||
const agentId = ctx.agentId;
|
||||
if (!agentId) return { ok: false, error: 'no agent context' };
|
||||
const entry = identity.findByAgentId(agentId);
|
||||
if (!entry) return { ok: false, error: `agent ${agentId} not registered` };
|
||||
const session = await client.agentLogin(entry.fabricApiKey);
|
||||
const nameNeedle = (p.nameFilter ?? '').toLowerCase();
|
||||
const purposeNeedle = (p.purposeFilter ?? '').toLowerCase();
|
||||
const guilds = session.guilds.filter((g) => {
|
||||
if (nameNeedle && !g.name.toLowerCase().includes(nameNeedle)) return false;
|
||||
if (purposeNeedle) {
|
||||
const purp = (g.purpose ?? '').toLowerCase();
|
||||
if (!purp.includes(purposeNeedle)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
count: guilds.length,
|
||||
guilds: guilds.map((g) => ({
|
||||
nodeId: g.nodeId,
|
||||
name: g.name,
|
||||
status: g.status,
|
||||
purpose: g.purpose ?? null,
|
||||
})),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// fabric-channel-set-purpose: set/update a channel's free-form
|
||||
// purpose description. Caller must be a channel member (or the
|
||||
// channel must be public). Use this to backfill purpose on existing
|
||||
// channels, or to refine it after a channel's role evolves.
|
||||
// -----------------------------------------------------------------
|
||||
api.registerTool((ctx: Ctx) => ({
|
||||
name: 'fabric-channel-set-purpose',
|
||||
description:
|
||||
"Set or update a channel's free-form purpose description. " +
|
||||
'Channel membership required (or the channel must be public). ' +
|
||||
'Pass empty string to clear. Use this to make a channel ' +
|
||||
'discoverable to other agents via fabric-channel-list.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['guildNodeId', 'channelId', 'purpose'],
|
||||
properties: {
|
||||
guildNodeId: { type: 'string' },
|
||||
channelId: { type: 'string' },
|
||||
purpose: {
|
||||
type: 'string',
|
||||
description: "What this channel is for. Pass '' (empty string) to clear.",
|
||||
},
|
||||
},
|
||||
},
|
||||
execute: async (p: { guildNodeId: string; channelId: string; purpose: string }) => {
|
||||
const agentId = ctx.agentId;
|
||||
if (!agentId) return { ok: false, error: 'no agent context' };
|
||||
const { guild, token } = await ctxGuild(agentId, p.guildNodeId);
|
||||
const res = await client.setChannelPurpose(
|
||||
guild.endpoint,
|
||||
token,
|
||||
p.channelId,
|
||||
p.purpose,
|
||||
);
|
||||
return { ok: true, channel: res };
|
||||
},
|
||||
}));
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// fabric-message-history: read a channel's recent message history by
|
||||
// `seq`. Tail-by-default: when `seqFrom`/`seqTo` are omitted, returns
|
||||
|
||||
Reference in New Issue
Block a user