182 lines
6.2 KiB
TypeScript
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';
|
|
}
|
|
}
|