import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import type { AmqOperations, AmqOperationResult } from "../transport/amq-operations.js"; export const MUTATING_AMQ_TOOLS = ["amq_send", "amq_reply", "amq_get_message", "amq_set_status"] as const; export const NON_CONSUMING_AMQ_TOOLS = [ "amq_inbox", "amq_thread", "amq_receipts", "amq_presence_list", "amq_dlq_list", "amq_dlq_read", ] as const; export const ALL_AMQ_TOOLS = [...MUTATING_AMQ_TOOLS, ...NON_CONSUMING_AMQ_TOOLS] as const; export interface AmqToolProvider { operations(): AmqOperations | undefined; canConsume(): boolean; canSetPresence(): boolean; } const Priority = Type.Union([ Type.Literal("urgent"), Type.Literal("normal"), Type.Literal("low"), ]); const Kind = Type.Union([ Type.Literal("brainstorm"), Type.Literal("review_request"), Type.Literal("review_response"), Type.Literal("question"), Type.Literal("answer"), Type.Literal("decision"), Type.Literal("status"), Type.Literal("todo"), ]); const PresenceStatus = Type.Union([ Type.Literal("idle"), Type.Literal("busy"), Type.Literal("paused"), Type.Literal("offline"), ]); const Context = Type.Record(Type.String(), Type.Unknown()); export function registerAmqTools(pi: ExtensionAPI, provider: AmqToolProvider): void { pi.registerTool({ name: "amq_send", label: "AMQ Send", description: "Send one AMQ message to explicit recipients. Never retries an uncertain send.", promptSnippet: "Send AMQ mail to explicit recipients", parameters: Type.Object({ to: Type.Array(Type.String(), { minItems: 1, maxItems: 32 }), body: Type.String(), subject: Type.Optional(Type.String()), kind: Type.Optional(Kind), priority: Type.Optional(Priority), labels: Type.Optional(Type.Array(Type.String(), { maxItems: 32 })), context: Type.Optional(Context), thread: Type.Optional(Type.String()), project: Type.Optional(Type.String()), session: Type.Optional(Type.String()), wait_for_drained: Type.Optional(Type.Boolean()), wait_timeout: Type.Optional(Type.String()), }), async execute(_id, params, signal) { return execute(provider, (operations) => operations.send({ to: params.to, body: params.body, ...(params.subject ? { subject: params.subject } : {}), ...(params.kind ? { kind: params.kind } : {}), ...(params.priority ? { priority: params.priority } : {}), ...(params.labels ? { labels: params.labels } : {}), ...(params.context ? { context: params.context } : {}), ...(params.thread ? { thread: params.thread } : {}), ...(params.project ? { project: params.project } : {}), ...(params.session ? { session: params.session } : {}), ...(params.wait_for_drained !== undefined ? { waitForDrained: params.wait_for_drained } : {}), ...(params.wait_timeout ? { waitTimeout: params.wait_timeout } : {}), }, signal)); }, }); pi.registerTool({ name: "amq_reply", label: "AMQ Reply", description: "Reply using the immutable original AMQ message ID; AMQ owns recipients and thread routing.", promptSnippet: "Reply through an exact AMQ message ID", parameters: Type.Object({ id: Type.String(), body: Type.String(), subject: Type.Optional(Type.String()), kind: Type.Optional(Kind), priority: Type.Optional(Priority), labels: Type.Optional(Type.Array(Type.String(), { maxItems: 32 })), context: Type.Optional(Context), wait_for_drained: Type.Optional(Type.Boolean()), wait_timeout: Type.Optional(Type.String()), }), async execute(_id, params, signal) { return execute(provider, (operations) => operations.reply({ id: params.id, body: params.body, ...(params.subject ? { subject: params.subject } : {}), ...(params.kind ? { kind: params.kind } : {}), ...(params.priority ? { priority: params.priority } : {}), ...(params.labels ? { labels: params.labels } : {}), ...(params.context ? { context: params.context } : {}), ...(params.wait_for_drained !== undefined ? { waitForDrained: params.wait_for_drained } : {}), ...(params.wait_timeout ? { waitTimeout: params.wait_timeout } : {}), }, signal)); }, }); pi.registerTool({ name: "amq_inbox", label: "AMQ Inbox", description: "List bounded AMQ new or cur message headers without exposing queue paths.", parameters: Type.Object({ box: Type.Optional(Type.Union([Type.Literal("new"), Type.Literal("cur")])), from: Type.Optional(Type.String()), kind: Type.Optional(Kind), priority: Type.Optional(Priority), labels: Type.Optional(Type.Array(Type.String(), { maxItems: 32 })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })), offset: Type.Optional(Type.Integer({ minimum: 0 })), }), async execute(_id, params, signal) { return execute(provider, (operations) => operations.inbox(params, signal)); }, }); pi.registerTool({ name: "amq_get_message", label: "AMQ Get Message", description: "Retrieve a bounded UTF-8 body slice by exact ID; new mail is claimed only through the receiver.", parameters: Type.Object({ id: Type.String(), offset_bytes: Type.Optional(Type.Integer({ minimum: 0 })), maximum_bytes: Type.Optional(Type.Integer({ minimum: 1, maximum: 24576 })), }), async execute(_id, params, signal) { if (!provider.canConsume()) return unavailable("amq_get_message requires the active receiver lease"); return execute(provider, (operations) => operations.getMessage({ id: params.id, ...(params.offset_bytes !== undefined ? { offsetBytes: params.offset_bytes } : {}), ...(params.maximum_bytes !== undefined ? { maximumBytes: params.maximum_bytes } : {}), }, signal)); }, }); pi.registerTool({ name: "amq_thread", label: "AMQ Thread", description: "Read a bounded AMQ thread projection.", parameters: Type.Object({ id: Type.String(), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })) }), async execute(_id, params, signal) { return execute(provider, (operations) => operations.thread(params.id, params.limit, signal)); }, }); pi.registerTool({ name: "amq_receipts", label: "AMQ Receipts", description: "List or wait for drained/DLQ receipt evidence without resending.", parameters: Type.Object({ message_id: Type.String(), wait: Type.Optional(Type.Boolean()), stage: Type.Optional(Type.Union([Type.Literal("drained"), Type.Literal("dlq")])), timeout: Type.Optional(Type.String()), }), async execute(_id, params, signal) { return execute(provider, (operations) => operations.receipts(params.message_id, params, signal)); }, }); pi.registerTool({ name: "amq_presence_list", label: "AMQ Presence", description: "Inspect advisory AMQ roster presence.", parameters: Type.Object({}), async execute(_id, _params, signal) { return execute(provider, (operations) => operations.presenceList(signal)); }, }); pi.registerTool({ name: "amq_set_status", label: "AMQ Set Status", description: "Set this receiving handle's advisory presence status and note.", parameters: Type.Object({ status: PresenceStatus, note: Type.Optional(Type.String()) }), async execute(_id, params, signal) { if (!provider.canSetPresence()) return unavailable("amq_set_status is disabled outside the owning receiver"); return execute(provider, (operations) => operations.setStatus(params.status, params.note, signal)); }, }); pi.registerTool({ name: "amq_dlq_list", label: "AMQ DLQ", description: "List bounded AMQ dead-letter diagnostics; does not purge or retry.", parameters: Type.Object({ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })) }), async execute(_id, params, signal) { return execute(provider, (operations) => operations.dlqList(params.limit, signal)); }, }); pi.registerTool({ name: "amq_dlq_read", label: "AMQ DLQ Read", description: "Read one bounded AMQ dead-letter diagnostic; does not purge or retry.", parameters: Type.Object({ id: Type.String() }), async execute(_id, params, signal) { return execute(provider, (operations) => operations.dlqRead(params.id, signal)); }, }); } async function execute( provider: AmqToolProvider, operation: (operations: AmqOperations) => Promise>, ): Promise> { const operations = provider.operations(); if (!operations) return unavailable("pi-amq is dormant; use /amq bind"); try { const result = await operation(operations); return result.ok ? rendered(result.value) : rendered({ error: result.error }); } catch (error) { return rendered({ error: { category: "extension", message: toError(error).message } }); } } function unavailable(message: string): AgentToolResult { return rendered({ error: { category: "unavailable", message } }); } function rendered(details: unknown): AgentToolResult { return { content: [{ type: "text", text: JSON.stringify(details, null, 2) }], details, }; } function toError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)); }