Files
hub/ts/tools/classes.toolregistry.ts
2026-05-05 12:03:45 +00:00

182 lines
6.2 KiB
TypeScript

import * as plugins from '../plugins.js';
import type { AgentRegistry } from '../agents/classes.agentregistry.js';
import type { ApprovalQueue } from '../approvals/classes.approvalqueue.js';
import type { AuditLog } from '../audit/classes.auditlog.js';
import type { DeviceRegistry } from '../devices/classes.deviceregistry.js';
export class ToolRegistry {
constructor(
private deviceRegistry: DeviceRegistry,
private agentRegistry: AgentRegistry,
private approvalQueue: ApprovalQueue,
private auditLog: AuditLog
) {}
public listTools(filterArg: { ownerId?: string } = {}) {
const deviceTools = this.deviceRegistry.listDevices().flatMap((deviceArg) => this.createDeviceTools(deviceArg));
const agentTools = this.agentRegistry.listAgents().map((agentArg) => this.createAgentTool(agentArg));
return [...deviceTools, ...agentTools].filter((toolArg) => !filterArg.ownerId || toolArg.ownerId === filterArg.ownerId);
}
public async executePlan(planArg: plugins.shxInterfaces.data.IToolPlan): Promise<plugins.shxInterfaces.data.IToolPlanResult> {
const tools = this.listTools();
const results: plugins.shxInterfaces.data.IToolExecutionResult[] = [];
for (const call of planArg.calls) {
const tool = tools.find((toolArg) => toolArg.id === call.toolId);
if (!tool) {
results.push({
callId: call.id,
status: 'failed',
errorMessage: `Tool not found: ${call.toolId}`,
});
continue;
}
if (tool.mode === 'ask') {
const approval = this.approvalQueue.createApproval({
plan: planArg,
call,
agentId: tool.ownerId,
title: planArg.title,
reason: planArg.reason,
requestedScopes: tool.requiredScopes,
});
results.push({
callId: call.id,
status: 'queuedForApproval',
approvalId: approval.id,
});
continue;
}
if (tool.mode === 'suggest') {
results.push({
callId: call.id,
status: 'suggested',
output: {
message: `${tool.name} returned a suggestion only.`,
},
});
continue;
}
const output = this.executeAutoTool(tool, call);
this.auditLog.appendReceipt({
kind: 'tool',
callerId: call.callerId,
toolId: tool.id,
scope: tool.requiredScopes.join(','),
inputSummary: JSON.stringify(call.input),
outputSummary: JSON.stringify(output),
reversible: tool.id.includes(':write'),
});
results.push({
callId: call.id,
status: 'executed',
output,
});
}
return {
planId: planArg.id,
results,
};
}
private createDeviceTools(deviceArg: plugins.shxInterfaces.data.IDeviceDefinition): plugins.shxInterfaces.data.IToolDefinition[] {
const tools: plugins.shxInterfaces.data.IToolDefinition[] = [
{
id: `device:${deviceArg.id}:read`,
ownerId: deviceArg.id,
name: `Read ${deviceArg.name}`,
description: `Read current state from ${deviceArg.name}.`,
inputSchema: { type: 'object', properties: {} },
requiredScopes: ['device.read'],
mode: 'auto',
},
];
for (const feature of deviceArg.features.filter((featureArg: plugins.shxInterfaces.data.IDeviceFeature) => featureArg.writable)) {
const mode = feature.capability === 'lock' ? 'ask' : 'auto';
const scope = this.scopeForCapability(feature.capability, true);
tools.push({
id: `device:${deviceArg.id}:${feature.id}:write`,
ownerId: deviceArg.id,
name: `Set ${deviceArg.name} ${feature.name}`,
description: `Write ${feature.name} on ${deviceArg.name}.`,
inputSchema: {
type: 'object',
properties: {
value: { type: ['string', 'number', 'boolean', 'object', 'null'] },
},
required: ['value'],
},
requiredScopes: [scope],
mode,
});
}
return tools;
}
private createAgentTool(agentArg: plugins.shxInterfaces.data.IAgentDefinition): plugins.shxInterfaces.data.IToolDefinition {
return {
id: `agent:${agentArg.id}:decide`,
ownerId: agentArg.id,
name: `Ask ${agentArg.name}`,
description: agentArg.role,
inputSchema: {
type: 'object',
properties: {
goal: { type: 'string' },
scopes: { type: 'array', items: { type: 'string' } },
},
required: ['goal'],
},
requiredScopes: ['agent.invoke'],
mode: agentArg.mode,
};
}
private executeAutoTool(toolArg: plugins.shxInterfaces.data.IToolDefinition, callArg: plugins.shxInterfaces.data.IToolCall) {
if (toolArg.id.startsWith('device:') && toolArg.id.endsWith(':read')) {
const deviceId = String(callArg.input.deviceId || toolArg.ownerId);
return {
device: this.deviceRegistry.getDeviceById(deviceId),
};
}
if (toolArg.id.startsWith('device:') && toolArg.id.endsWith(':write')) {
const [, deviceId, featureId] = toolArg.id.split(':');
const state = this.deviceRegistry.updateDeviceState(
deviceId,
featureId,
callArg.input.value as plugins.shxInterfaces.data.TDeviceStateValue
);
return { state };
}
if (toolArg.id.startsWith('agent:')) {
this.agentRegistry.recordAction(toolArg.ownerId, String(callArg.input.goal || 'Agent invoked'));
return {
message: `${toolArg.name} accepted the task.`,
};
}
return {
message: `${toolArg.name} executed.`,
};
}
private scopeForCapability(capabilityArg: plugins.shxInterfaces.data.TDeviceCapability, writeArg: boolean) {
const suffix = writeArg ? 'write' : 'read';
if (capabilityArg === 'light') return `light.${suffix}` as plugins.shxInterfaces.data.TToolScope;
if (capabilityArg === 'climate') return `climate.${suffix}` as plugins.shxInterfaces.data.TToolScope;
if (capabilityArg === 'energy') return `energy.${suffix}` as plugins.shxInterfaces.data.TToolScope;
if (capabilityArg === 'lock') return `lock.${suffix}` as plugins.shxInterfaces.data.TToolScope;
if (capabilityArg === 'camera') return 'camera.read';
return writeArg ? 'device.write' : 'device.read';
}
}