/** * Manifest Route * * GET /.well-known/manifest.json - Agent self-description and capability discovery */ import { Hono } from "hono"; import type { Runtime, Action } from "@gizmo-ai/runtime"; import type { GizmoManifest, ManifestConfig, ErrorResponse, ServerRoute } from "../types.ts"; import type { SchemaRegistry } from "../schema-registry.ts"; export interface ManifestRouteConfig, A extends Action> { runtime: Runtime; manifestConfig?: ManifestConfig; basePath?: string; historyEnabled: boolean; /** Schema registry for static manifest generation */ schemaRegistry?: SchemaRegistry; /** * Whether to infer state schema from runtime (default: false when schemaRegistry provided) * Set to true to always use runtime inference even when schemaRegistry is available. */ inferState?: boolean; /** Additional routes to include in manifest (for routes not in schemaRegistry) */ routes?: ServerRoute[]; } /** * Create the manifest route */ export function createManifestRoute< S extends Record, A extends Action >(config: ManifestRouteConfig): Hono { const { runtime, manifestConfig, basePath = "", historyEnabled, schemaRegistry, inferState, routes = [], } = config; // Determine whether to use static schemas or inference // Default to static when registry provided, unless inferState=true const useStaticSchemas = schemaRegistry && inferState !== true; // Compute fallback agentId once at route creation time so it's stable across requests const fallbackAgentId = manifestConfig?.identity?.agentId ?? generateAgentId(); const app = new Hono(); app.get("/", (c) => { try { const manifest = useStaticSchemas ? generateManifestFromRegistry( runtime, schemaRegistry, manifestConfig, basePath, historyEnabled, routes, fallbackAgentId ) : generateManifest( runtime, manifestConfig, basePath, historyEnabled, fallbackAgentId ); c.header("Content-Type", "application/json"); c.header("Cache-Control", "public, max-age=300"); // Cache 5 minutes return c.json(manifest); } catch (error) { return c.json( { error: `Failed to generate manifest: ${ error instanceof Error ? error.message : String(error) }` }, 500 ); } }); return app; } function generateManifest, A extends Action>( runtime: Runtime, config: ManifestConfig | undefined, basePath: string, historyEnabled: boolean, agentId: string ): GizmoManifest { const state = runtime.getState() as Record; // Build endpoints const endpoints: GizmoManifest["endpoints"] = { invoke: `${basePath}/invoke`, state: `${basePath}/state`, runs: `${basePath}/runs`, streams: { state: `${basePath}/stream/state`, actions: `${basePath}/stream/actions`, events: `${basePath}/events`, }, control: { cancel: `${basePath}/runs/:id/cancel`, continue: `${basePath}/runs/:id/continue`, }, }; // Discover plugin routes from runtime const plugins = runtime.getPlugins(); const pluginEndpoints: Record = {}; for (const plugin of plugins) { if (plugin.routes) { const routePath = plugin.routes.path.startsWith('/') ? plugin.routes.path : `/${plugin.routes.path}`; // Use plugin key as the endpoint name, fallback to route path const endpointName = plugin.key || routePath.replace(/^\//, ''); pluginEndpoints[endpointName] = `${basePath}${routePath}`; } } if (Object.keys(pluginEndpoints).length > 0) { endpoints.plugins = pluginEndpoints; } // Get state slices (filter out prototype methods like toString) const slices = Object.keys(state).filter(key => typeof state[key] !== 'function' && !['toString', 'valueOf', 'hasOwnProperty'].includes(key) ); // Generate state schema const stateSchema = generateStateSchema(state, slices); // Extract capabilities from runtime const capabilities = extractCapabilities(runtime, state, historyEnabled); const manifest: GizmoManifest = { manifestVersion: "1.0.0", identity: { agentId, name: config?.identity?.name ?? "Gizmo Agent", version: config?.identity?.version ?? "0.1.0", description: config?.identity?.description, documentationUrl: config?.identity?.documentationUrl, }, endpoints, state: { slices, schemaVersion: "1.0.0", schema: stateSchema, }, actions: { publicContractsRef: `${basePath}/contracts/actions.public.json`, }, capabilities, }; if (config?.auth) { manifest.auth = config.auth; } return manifest; } /** * Generate JSON Schema from runtime state * * Creates an inline schema describing the state structure for UI discovery. * Uses $defs for each slice to keep the schema organized. */ function generateStateSchema( state: Record, slices: string[] ): GizmoManifest["state"]["schema"] { const properties: Record = {}; const $defs: Record = {}; for (const slice of slices) { const sliceValue = state[slice]; const sliceSchema = inferSchema(sliceValue, slice); // Reference the definition from properties properties[slice] = { $ref: `#/$defs/${slice}` }; $defs[slice] = sliceSchema; } return { type: "object", properties, $defs, }; } /** * Infer JSON Schema from a runtime value * * Creates a basic schema from the current value structure. * This is a simple inference - not a full type analysis. */ function inferSchema(value: unknown, name?: string): Record { if (value === null) { return { type: "null" }; } if (Array.isArray(value)) { // Infer array item type from first element (if available) const items = value.length > 0 ? inferSchema(value[0]) : { type: "object" }; return { type: "array", items, }; } if (typeof value === "object") { const obj = value as Record; const properties: Record = {}; for (const [key, val] of Object.entries(obj)) { // Skip function properties (tools have execute functions) if (typeof val === "function") continue; properties[key] = inferSchema(val, key); } return { type: "object", properties, }; } if (typeof value === "string") { return { type: "string" }; } if (typeof value === "number") { return { type: "number" }; } if (typeof value === "boolean") { return { type: "boolean" }; } // Fallback for unknown types return { type: "object" }; } /** Tool with a name property (minimal interface for extraction) */ interface NamedTool { name: string; } /** Agent state with optional tools array */ interface AgentStateWithTools { tools?: NamedTool[]; isStreaming?: boolean; } /** Skill metadata from skills state */ interface SkillMetadata { name: string; description?: string; source?: string; } /** Skills state with discovered skills */ interface SkillsState { discovered?: SkillMetadata[]; } function extractCapabilities, A extends Action>( _runtime: Runtime, state: Record, historyEnabled: boolean ): GizmoManifest["capabilities"] { const capabilities: GizmoManifest["capabilities"] = {}; // Extract tools from agent state const agentState = state.agent as AgentStateWithTools | undefined; if (agentState?.tools && Array.isArray(agentState.tools)) { capabilities.tools = agentState.tools.map((t) => t.name); } // Extract skills from skills state const skillsState = state.skills as SkillsState | undefined; if (skillsState?.discovered && Array.isArray(skillsState.discovered)) { capabilities.skills = skillsState.discovered.map((s) => ({ name: s.name, description: s.description, source: s.source, })); } // Detect features const features: string[] = []; // Check for streaming capability if (agentState?.isStreaming !== undefined) { features.push("streaming"); } // Check for tool execution if (agentState?.tools && agentState.tools.length > 0) { features.push("tool_execution"); } // Check if run history is enabled if (historyEnabled) { features.push("run_history"); } // Check for skills if (skillsState?.discovered && skillsState.discovered.length > 0) { features.push("skills"); } if (features.length > 0) { capabilities.features = features; } return capabilities; } function generateAgentId(): string { return `gizmo-${Date.now()}-${Math.random().toString(36).substring(7)}`; } /** * Generate manifest from static schema registry * * Uses plugin-provided schemas instead of runtime inference. * This produces a cleaner, more predictable manifest. */ function generateManifestFromRegistry< S extends Record, A extends Action >( runtime: Runtime, schemaRegistry: SchemaRegistry, config: ManifestConfig | undefined, basePath: string, historyEnabled: boolean, additionalRoutes: ServerRoute[], agentId: string ): GizmoManifest { const state = runtime.getState() as Record; // Build endpoints const endpoints: GizmoManifest["endpoints"] = { invoke: `${basePath}/invoke`, state: `${basePath}/state`, runs: `${basePath}/runs`, streams: { state: `${basePath}/stream/state`, actions: `${basePath}/stream/actions`, events: `${basePath}/events`, }, control: { cancel: `${basePath}/runs/:id/cancel`, continue: `${basePath}/runs/:id/continue`, }, }; // Discover plugin routes from runtime (same as inference mode) const plugins = runtime.getPlugins(); const pluginEndpoints: Record = {}; for (const plugin of plugins) { if (plugin.routes) { const routePath = plugin.routes.path.startsWith("/") ? plugin.routes.path : `/${plugin.routes.path}`; const endpointName = plugin.key || routePath.replace(/^\//, ""); pluginEndpoints[endpointName] = `${basePath}${routePath}`; } } // Also add any additional routes for (const route of additionalRoutes) { const routePath = route.path.startsWith("/") ? route.path : `/${route.path}`; const endpointName = routePath.replace(/^\//, ""); pluginEndpoints[endpointName] = `${basePath}${routePath}`; } if (Object.keys(pluginEndpoints).length > 0) { endpoints.plugins = pluginEndpoints; } // Get slice keys from schema registry const slices = schemaRegistry.getSliceKeys(); // Get state schema from registry (static, no inference) const stateSchema = schemaRegistry.getStateSchema(); // Get capabilities from registry, then augment with runtime state const registryCapabilities = schemaRegistry.getCapabilities(); const runtimeCapabilities = extractCapabilities(runtime, state, historyEnabled); // Merge capabilities (runtime values override/augment registry) const capabilities: GizmoManifest["capabilities"] = { ...registryCapabilities, // Use runtime tools (actual registered tools) over static declaration tools: runtimeCapabilities.tools || registryCapabilities.tools, // Use runtime skills (actual discovered skills) skills: runtimeCapabilities.skills, // Merge features (union of static + runtime detected) features: mergeFeatures( registryCapabilities.features, runtimeCapabilities.features ), }; const manifest: GizmoManifest = { manifestVersion: "1.0.0", identity: { agentId, name: config?.identity?.name ?? "Gizmo Agent", version: config?.identity?.version ?? "0.1.0", description: config?.identity?.description, documentationUrl: config?.identity?.documentationUrl, }, endpoints, state: { slices, schemaVersion: "1.0.0", schema: stateSchema as GizmoManifest["state"]["schema"], }, actions: { publicContractsRef: `${basePath}/contracts/actions.public.json`, }, capabilities, }; if (config?.auth) { manifest.auth = config.auth; } return manifest; } /** * Merge feature arrays, deduplicating */ function mergeFeatures( staticFeatures: string[] | undefined, runtimeFeatures: string[] | undefined ): string[] | undefined { const features = new Set(); if (staticFeatures) { for (const f of staticFeatures) { features.add(f); } } if (runtimeFeatures) { for (const f of runtimeFeatures) { features.add(f); } } return features.size > 0 ? Array.from(features) : undefined; }