Add hub package
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user