@kilnai/runtime 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a/a2a-client.d.ts +6 -0
- package/dist/a2a/a2a-client.d.ts.map +1 -0
- package/dist/a2a/a2a-client.js +112 -0
- package/dist/a2a/a2a-client.js.map +1 -0
- package/dist/a2a/index.d.ts +2 -0
- package/dist/a2a/index.d.ts.map +1 -0
- package/dist/a2a/index.js +3 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/channels/api-channel.d.ts +43 -0
- package/dist/channels/api-channel.d.ts.map +1 -0
- package/dist/channels/api-channel.js +95 -0
- package/dist/channels/api-channel.js.map +1 -0
- package/dist/channels/channel-registry.d.ts +25 -0
- package/dist/channels/channel-registry.d.ts.map +1 -0
- package/dist/channels/channel-registry.js +49 -0
- package/dist/channels/channel-registry.js.map +1 -0
- package/dist/channels/channel-router.d.ts +43 -0
- package/dist/channels/channel-router.d.ts.map +1 -0
- package/dist/channels/channel-router.js +72 -0
- package/dist/channels/channel-router.js.map +1 -0
- package/dist/channels/cli-channel.d.ts +19 -0
- package/dist/channels/cli-channel.d.ts.map +1 -0
- package/dist/channels/cli-channel.js +37 -0
- package/dist/channels/cli-channel.js.map +1 -0
- package/dist/channels/event-bridge.d.ts +27 -0
- package/dist/channels/event-bridge.d.ts.map +1 -0
- package/dist/channels/event-bridge.js +83 -0
- package/dist/channels/event-bridge.js.map +1 -0
- package/dist/channels/index.d.ts +17 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +12 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/message-formatter.d.ts +4 -0
- package/dist/channels/message-formatter.d.ts.map +1 -0
- package/dist/channels/message-formatter.js +32 -0
- package/dist/channels/message-formatter.js.map +1 -0
- package/dist/channels/slack-channel.d.ts +32 -0
- package/dist/channels/slack-channel.d.ts.map +1 -0
- package/dist/channels/slack-channel.js +71 -0
- package/dist/channels/slack-channel.js.map +1 -0
- package/dist/channels/types.d.ts +27 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +12 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/channels/web-channel.d.ts +37 -0
- package/dist/channels/web-channel.d.ts.map +1 -0
- package/dist/channels/web-channel.js +112 -0
- package/dist/channels/web-channel.js.map +1 -0
- package/dist/channels/whatsapp-api.d.ts +20 -0
- package/dist/channels/whatsapp-api.d.ts.map +1 -0
- package/dist/channels/whatsapp-api.js +44 -0
- package/dist/channels/whatsapp-api.js.map +1 -0
- package/dist/channels/whatsapp-channel.d.ts +32 -0
- package/dist/channels/whatsapp-channel.d.ts.map +1 -0
- package/dist/channels/whatsapp-channel.js +85 -0
- package/dist/channels/whatsapp-channel.js.map +1 -0
- package/dist/gateway/app-resolver.d.ts +11 -0
- package/dist/gateway/app-resolver.d.ts.map +1 -0
- package/dist/gateway/app-resolver.js +36 -0
- package/dist/gateway/app-resolver.js.map +1 -0
- package/dist/gateway/approval-registry.d.ts +19 -0
- package/dist/gateway/approval-registry.d.ts.map +1 -0
- package/dist/gateway/approval-registry.js +49 -0
- package/dist/gateway/approval-registry.js.map +1 -0
- package/dist/gateway/budget-middleware.d.ts +47 -0
- package/dist/gateway/budget-middleware.d.ts.map +1 -0
- package/dist/gateway/budget-middleware.js +88 -0
- package/dist/gateway/budget-middleware.js.map +1 -0
- package/dist/gateway/config-validator.d.ts +31 -0
- package/dist/gateway/config-validator.d.ts.map +1 -0
- package/dist/gateway/config-validator.js +68 -0
- package/dist/gateway/config-validator.js.map +1 -0
- package/dist/gateway/delegation-handler.d.ts +53 -0
- package/dist/gateway/delegation-handler.d.ts.map +1 -0
- package/dist/gateway/delegation-handler.js +257 -0
- package/dist/gateway/delegation-handler.js.map +1 -0
- package/dist/gateway/delegation-routes.d.ts +7 -0
- package/dist/gateway/delegation-routes.d.ts.map +1 -0
- package/dist/gateway/delegation-routes.js +48 -0
- package/dist/gateway/delegation-routes.js.map +1 -0
- package/dist/gateway/dev-inspector.d.ts +2 -0
- package/dist/gateway/dev-inspector.d.ts.map +1 -0
- package/dist/gateway/dev-inspector.js +355 -0
- package/dist/gateway/dev-inspector.js.map +1 -0
- package/dist/gateway/dev-orchestrator.d.ts +24 -0
- package/dist/gateway/dev-orchestrator.d.ts.map +1 -0
- package/dist/gateway/dev-orchestrator.js +71 -0
- package/dist/gateway/dev-orchestrator.js.map +1 -0
- package/dist/gateway/dev-routes-types.d.ts +39 -0
- package/dist/gateway/dev-routes-types.d.ts.map +1 -0
- package/dist/gateway/dev-routes-types.js +3 -0
- package/dist/gateway/dev-routes-types.js.map +1 -0
- package/dist/gateway/dev-routes.d.ts +53 -0
- package/dist/gateway/dev-routes.d.ts.map +1 -0
- package/dist/gateway/dev-routes.js +217 -0
- package/dist/gateway/dev-routes.js.map +1 -0
- package/dist/gateway/dev-token-store.d.ts +19 -0
- package/dist/gateway/dev-token-store.d.ts.map +1 -0
- package/dist/gateway/dev-token-store.js +40 -0
- package/dist/gateway/dev-token-store.js.map +1 -0
- package/dist/gateway/gateway-routes.d.ts +46 -0
- package/dist/gateway/gateway-routes.d.ts.map +1 -0
- package/dist/gateway/gateway-routes.js +143 -0
- package/dist/gateway/gateway-routes.js.map +1 -0
- package/dist/gateway/gateway-server.d.ts +25 -0
- package/dist/gateway/gateway-server.d.ts.map +1 -0
- package/dist/gateway/gateway-server.js +736 -0
- package/dist/gateway/gateway-server.js.map +1 -0
- package/dist/gateway/health-registry.d.ts +18 -0
- package/dist/gateway/health-registry.d.ts.map +1 -0
- package/dist/gateway/health-registry.js +40 -0
- package/dist/gateway/health-registry.js.map +1 -0
- package/dist/gateway/memory-routes.d.ts +12 -0
- package/dist/gateway/memory-routes.d.ts.map +1 -0
- package/dist/gateway/memory-routes.js +32 -0
- package/dist/gateway/memory-routes.js.map +1 -0
- package/dist/gateway/mode-b-routes.d.ts +14 -0
- package/dist/gateway/mode-b-routes.d.ts.map +1 -0
- package/dist/gateway/mode-b-routes.js +96 -0
- package/dist/gateway/mode-b-routes.js.map +1 -0
- package/dist/gateway/safety-middleware.d.ts +21 -0
- package/dist/gateway/safety-middleware.d.ts.map +1 -0
- package/dist/gateway/safety-middleware.js +175 -0
- package/dist/gateway/safety-middleware.js.map +1 -0
- package/dist/gateway/security-middleware.d.ts +12 -0
- package/dist/gateway/security-middleware.d.ts.map +1 -0
- package/dist/gateway/security-middleware.js +62 -0
- package/dist/gateway/security-middleware.js.map +1 -0
- package/dist/gateway/tenant-admin-routes.d.ts +15 -0
- package/dist/gateway/tenant-admin-routes.d.ts.map +1 -0
- package/dist/gateway/tenant-admin-routes.js +148 -0
- package/dist/gateway/tenant-admin-routes.js.map +1 -0
- package/dist/gateway/tenant-routes.d.ts +15 -0
- package/dist/gateway/tenant-routes.d.ts.map +1 -0
- package/dist/gateway/tenant-routes.js +107 -0
- package/dist/gateway/tenant-routes.js.map +1 -0
- package/dist/gateway/whatsapp-webhook-routes.d.ts +15 -0
- package/dist/gateway/whatsapp-webhook-routes.d.ts.map +1 -0
- package/dist/gateway/whatsapp-webhook-routes.js +217 -0
- package/dist/gateway/whatsapp-webhook-routes.js.map +1 -0
- package/dist/gateway/ws-routes.d.ts +19 -0
- package/dist/gateway/ws-routes.d.ts.map +1 -0
- package/dist/gateway/ws-routes.js +79 -0
- package/dist/gateway/ws-routes.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/session/index.d.ts +6 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +4 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/mode-b-orchestrator.d.ts +43 -0
- package/dist/session/mode-b-orchestrator.d.ts.map +1 -0
- package/dist/session/mode-b-orchestrator.js +224 -0
- package/dist/session/mode-b-orchestrator.js.map +1 -0
- package/dist/session/mode-b-session.d.ts +29 -0
- package/dist/session/mode-b-session.d.ts.map +1 -0
- package/dist/session/mode-b-session.js +50 -0
- package/dist/session/mode-b-session.js.map +1 -0
- package/dist/session/session-registry.d.ts +15 -0
- package/dist/session/session-registry.d.ts.map +1 -0
- package/dist/session/session-registry.js +60 -0
- package/dist/session/session-registry.js.map +1 -0
- package/dist/tenant/index.d.ts +3 -0
- package/dist/tenant/index.d.ts.map +1 -0
- package/dist/tenant/index.js +3 -0
- package/dist/tenant/index.js.map +1 -0
- package/dist/tenant/system-prompt-builder.d.ts +3 -0
- package/dist/tenant/system-prompt-builder.d.ts.map +1 -0
- package/dist/tenant/system-prompt-builder.js +76 -0
- package/dist/tenant/system-prompt-builder.js.map +1 -0
- package/dist/tenant/tenant-registry.d.ts +33 -0
- package/dist/tenant/tenant-registry.d.ts.map +1 -0
- package/dist/tenant/tenant-registry.js +156 -0
- package/dist/tenant/tenant-registry.js.map +1 -0
- package/dist/trigger/event-listener.d.ts +22 -0
- package/dist/trigger/event-listener.d.ts.map +1 -0
- package/dist/trigger/event-listener.js +64 -0
- package/dist/trigger/event-listener.js.map +1 -0
- package/dist/trigger/index.d.ts +10 -0
- package/dist/trigger/index.d.ts.map +1 -0
- package/dist/trigger/index.js +7 -0
- package/dist/trigger/index.js.map +1 -0
- package/dist/trigger/scheduler.d.ts +22 -0
- package/dist/trigger/scheduler.d.ts.map +1 -0
- package/dist/trigger/scheduler.js +77 -0
- package/dist/trigger/scheduler.js.map +1 -0
- package/dist/trigger/trigger-executor.d.ts +14 -0
- package/dist/trigger/trigger-executor.d.ts.map +1 -0
- package/dist/trigger/trigger-executor.js +47 -0
- package/dist/trigger/trigger-executor.js.map +1 -0
- package/dist/trigger/trigger-registry.d.ts +28 -0
- package/dist/trigger/trigger-registry.d.ts.map +1 -0
- package/dist/trigger/trigger-registry.js +118 -0
- package/dist/trigger/trigger-registry.js.map +1 -0
- package/dist/trigger/webhook-handler.d.ts +11 -0
- package/dist/trigger/webhook-handler.d.ts.map +1 -0
- package/dist/trigger/webhook-handler.js +71 -0
- package/dist/trigger/webhook-handler.js.map +1 -0
- package/dist/utils/hmac.d.ts +15 -0
- package/dist/utils/hmac.d.ts.map +1 -0
- package/dist/utils/hmac.js +27 -0
- package/dist/utils/hmac.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
// Gateway: GatewayServer -- persistent Bun/Hono process hosting multiple Apps
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { createBunWebSocket, serveStatic } from "hono/bun";
|
|
6
|
+
import { parseGatewayYaml, parseAppYaml, AnthropicAdapter, OpenAIAdapter, DeepSeekAdapter, OllamaAdapter, KilnError, OTelExporter, SafetyPipeline, SqliteMemoryStore, } from "@kilnai/core";
|
|
7
|
+
import { EventBus, McpClient, CostTracker } from "@kilnai/core";
|
|
8
|
+
import { ChannelRegistry } from "../channels/channel-registry.js";
|
|
9
|
+
import { WebChannel } from "../channels/web-channel.js";
|
|
10
|
+
import { TriggerRegistry } from "../trigger/trigger-registry.js";
|
|
11
|
+
import { resolveApps } from "./app-resolver.js";
|
|
12
|
+
import { createGatewayApp } from "./gateway-routes.js";
|
|
13
|
+
import { ModeBOrchestrator } from "../session/mode-b-orchestrator.js";
|
|
14
|
+
import { SessionRegistry } from "../session/session-registry.js";
|
|
15
|
+
import { TenantRegistry } from "../tenant/tenant-registry.js";
|
|
16
|
+
import { assertValidStartupConfig } from "./config-validator.js";
|
|
17
|
+
import { HealthRegistry } from "./health-registry.js";
|
|
18
|
+
import { ApprovalGateRegistry } from "./approval-registry.js";
|
|
19
|
+
import { DevOrchestrator } from "./dev-orchestrator.js";
|
|
20
|
+
import { DevTokenStore } from "./dev-token-store.js";
|
|
21
|
+
export { createGatewayApp } from "./gateway-routes.js";
|
|
22
|
+
export { createDevRoutes } from "./dev-routes.js";
|
|
23
|
+
export { createDevInspectorHtml } from "./dev-inspector.js";
|
|
24
|
+
export { ApprovalGateRegistry } from "./approval-registry.js";
|
|
25
|
+
export { DevOrchestrator } from "./dev-orchestrator.js";
|
|
26
|
+
export { DevTokenStore } from "./dev-token-store.js";
|
|
27
|
+
/** Build the shared portion of DevRoutesConfig from common dependencies */
|
|
28
|
+
function buildSharedDevRoutesConfig(deps) {
|
|
29
|
+
const { eventBus, costTracker, approvalRegistry, devOrchestrator, tokenStore } = deps;
|
|
30
|
+
return {
|
|
31
|
+
getEventBus: () => eventBus,
|
|
32
|
+
getCostSummary: () => costTracker.summary,
|
|
33
|
+
approvePhase: (sessionId) => approvalRegistry.approve(sessionId),
|
|
34
|
+
rejectPhase: (reason, sessionId) => approvalRegistry.reject(reason, sessionId),
|
|
35
|
+
startRun: devOrchestrator
|
|
36
|
+
? (task) => {
|
|
37
|
+
if (devOrchestrator.isRunning)
|
|
38
|
+
return { error: "A run is already in progress" };
|
|
39
|
+
const sessionId = devOrchestrator.start(task);
|
|
40
|
+
return { sessionId };
|
|
41
|
+
}
|
|
42
|
+
: undefined,
|
|
43
|
+
getRunStatus: devOrchestrator
|
|
44
|
+
? () => ({
|
|
45
|
+
sessionId: devOrchestrator.orchestrator.sessionId,
|
|
46
|
+
status: devOrchestrator.orchestrator.status,
|
|
47
|
+
phase: devOrchestrator.orchestrator.currentPhase,
|
|
48
|
+
task: devOrchestrator.orchestrator.task,
|
|
49
|
+
})
|
|
50
|
+
: undefined,
|
|
51
|
+
issueToken: tokenStore ? (userId) => tokenStore.issue(userId) : undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function resolveStudioDist() {
|
|
55
|
+
try {
|
|
56
|
+
const require = createRequire(import.meta.url);
|
|
57
|
+
const pkgPath = require.resolve("@kilnai/studio/package.json");
|
|
58
|
+
const distDir = join(dirname(pkgPath), "dist");
|
|
59
|
+
if (existsSync(join(distDir, "index.html")))
|
|
60
|
+
return distDir;
|
|
61
|
+
console.warn("Studio: @kilnai/studio found but dist/ not built. Run `bun run build` in packages/studio.");
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
console.warn("Studio: @kilnai/studio not installed. Using inline dev inspector. Install it for the full Studio UI.");
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function startGateway(configPath, options) {
|
|
70
|
+
let content;
|
|
71
|
+
try {
|
|
72
|
+
content = readFileSync(configPath, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new KilnError("CONFIG_INVALID", `Failed to read gateway config: ${configPath}`, {
|
|
76
|
+
context: { configPath },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const gatewayConfig = parseGatewayYaml(content);
|
|
80
|
+
const gatewayYamlDir = dirname(configPath);
|
|
81
|
+
const resolvedApps = resolveApps(gatewayConfig, gatewayYamlDir);
|
|
82
|
+
// Build startup config validation input from resolved apps
|
|
83
|
+
const modeBApps = [];
|
|
84
|
+
let whatsappConfig;
|
|
85
|
+
let tenantAdminConfig;
|
|
86
|
+
for (const resolved of resolvedApps) {
|
|
87
|
+
if (resolved.modeBConfig?.runtime === "provider-adapter") {
|
|
88
|
+
const providerName = resolved.modeBConfig.provider.name;
|
|
89
|
+
const apiKeyEnv = resolved.modeBConfig.provider.apiKeyEnv;
|
|
90
|
+
if (apiKeyEnv) {
|
|
91
|
+
modeBApps.push({ provider: providerName, apiKeyEnv });
|
|
92
|
+
}
|
|
93
|
+
// Check for WhatsApp channel
|
|
94
|
+
const whatsappChannel = resolved.binding.channels.find((ch) => ch.type === "whatsapp");
|
|
95
|
+
if (whatsappChannel) {
|
|
96
|
+
const verifyTokenEnv = whatsappChannel.verifyTokenEnv ?? "";
|
|
97
|
+
const accessTokenEnv = whatsappChannel.accessTokenEnv ?? "";
|
|
98
|
+
if (verifyTokenEnv && accessTokenEnv) {
|
|
99
|
+
whatsappConfig = { verifyTokenEnv, accessTokenEnv };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Check for tenant admin
|
|
103
|
+
const adminChannel = resolved.binding.channels.find((ch) => ch.adminTokenEnv);
|
|
104
|
+
const adminTokenEnv = adminChannel?.adminTokenEnv ?? "";
|
|
105
|
+
if (adminTokenEnv) {
|
|
106
|
+
tenantAdminConfig = { adminTokenEnv };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Validate startup configuration before creating providers
|
|
111
|
+
assertValidStartupConfig({
|
|
112
|
+
modeBApps: modeBApps.length > 0 ? modeBApps : undefined,
|
|
113
|
+
whatsapp: whatsappConfig,
|
|
114
|
+
tenantAdmin: tenantAdminConfig,
|
|
115
|
+
});
|
|
116
|
+
// Initialize OTel exporter if observability is configured
|
|
117
|
+
// @opentelemetry/sdk-trace-base and @opentelemetry/exporter-trace-otlp-http are user-installed
|
|
118
|
+
// optional packages -- loaded via dynamic import so they're truly optional at compile time.
|
|
119
|
+
let otelExporter;
|
|
120
|
+
const obsConfig = gatewayConfig.observability;
|
|
121
|
+
if (obsConfig?.enabled) {
|
|
122
|
+
try {
|
|
123
|
+
const { trace } = await import("@opentelemetry/api");
|
|
124
|
+
let provider;
|
|
125
|
+
if (obsConfig.exporter === "console" || obsConfig.exporter === "otlp") {
|
|
126
|
+
const sdkModuleName = "@opentelemetry/sdk-trace-base";
|
|
127
|
+
const sdkBase = await import(sdkModuleName);
|
|
128
|
+
const p = new sdkBase.BasicTracerProvider();
|
|
129
|
+
if (obsConfig.exporter === "console") {
|
|
130
|
+
p.addSpanProcessor(new sdkBase.SimpleSpanProcessor(new sdkBase.ConsoleSpanExporter()));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const otlpModuleName = "@opentelemetry/exporter-trace-otlp-http";
|
|
134
|
+
const otlpMod = await import(otlpModuleName);
|
|
135
|
+
p.addSpanProcessor(new sdkBase.SimpleSpanProcessor(new otlpMod.OTLPTraceExporter({ url: obsConfig.endpoint })));
|
|
136
|
+
}
|
|
137
|
+
p.register();
|
|
138
|
+
provider = p;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// exporter: none -- use the global noop tracer provider
|
|
142
|
+
provider = trace.getTracerProvider();
|
|
143
|
+
}
|
|
144
|
+
otelExporter = new OTelExporter(provider, {
|
|
145
|
+
serviceName: obsConfig.serviceName,
|
|
146
|
+
attributes: obsConfig.attributes,
|
|
147
|
+
});
|
|
148
|
+
console.log(`Observability: OTel exporter "${obsConfig.exporter}" enabled for service "${obsConfig.serviceName}"`);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.warn(`Observability: failed to initialize OTel exporter -- ${err instanceof Error ? err.message : String(err)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// EventBus: shared across all apps for observability and dev inspector
|
|
155
|
+
const gatewayEventBus = new EventBus(100, otelExporter);
|
|
156
|
+
const costTracker = new CostTracker();
|
|
157
|
+
const loadedApps = resolvedApps.map((resolved) => {
|
|
158
|
+
const hasWebChannel = resolved.binding.channels.some((ch) => ch.type === "web");
|
|
159
|
+
return {
|
|
160
|
+
name: resolved.name,
|
|
161
|
+
app: resolved.app,
|
|
162
|
+
binding: resolved.binding,
|
|
163
|
+
registry: new ChannelRegistry(),
|
|
164
|
+
modeBRuntime: undefined,
|
|
165
|
+
tenantRuntime: undefined,
|
|
166
|
+
whatsappWebhookConfig: undefined,
|
|
167
|
+
tenantAdminConfig: undefined,
|
|
168
|
+
webChannel: hasWebChannel ? new WebChannel() : undefined,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
// Initialize Mode B runtimes and delegation targets in a single pass
|
|
172
|
+
const sessionRegistry = new SessionRegistry();
|
|
173
|
+
const delegationTargets = new Map();
|
|
174
|
+
for (const loaded of loadedApps) {
|
|
175
|
+
const resolved = resolvedApps.find((r) => r.name === loaded.name);
|
|
176
|
+
if (!resolved?.modeBConfig || resolved.modeBConfig.runtime !== "provider-adapter")
|
|
177
|
+
continue;
|
|
178
|
+
const provider = createProviderFromConfig(resolved.modeBConfig.provider);
|
|
179
|
+
const systemPrompt = buildSystemPromptFromApp(resolved.app);
|
|
180
|
+
// Discover MCP tools if configured
|
|
181
|
+
const mcpClients = [];
|
|
182
|
+
const tools = [];
|
|
183
|
+
if (resolved.app.mcp?.servers) {
|
|
184
|
+
for (const serverConfig of resolved.app.mcp.servers) {
|
|
185
|
+
try {
|
|
186
|
+
const client = new McpClient(serverConfig);
|
|
187
|
+
const capabilities = await client.discoverTools();
|
|
188
|
+
for (const cap of capabilities) {
|
|
189
|
+
tools.push({
|
|
190
|
+
name: cap.name,
|
|
191
|
+
description: cap.description,
|
|
192
|
+
inputSchema: cap.schema,
|
|
193
|
+
tags: new Set(cap.tags),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
mcpClients.push(client);
|
|
197
|
+
console.log(` ${loaded.name}: discovered ${capabilities.length} tools from MCP server "${serverConfig.name}"`);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
console.warn(` ${loaded.name}: failed to connect to MCP server "${serverConfig.name}": ${err}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const orchestrator = new ModeBOrchestrator({
|
|
205
|
+
provider,
|
|
206
|
+
model: resolved.modeBConfig.provider.model,
|
|
207
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
208
|
+
mcpClients: mcpClients.length > 0 ? mcpClients : undefined,
|
|
209
|
+
eventBus: gatewayEventBus,
|
|
210
|
+
});
|
|
211
|
+
// Register delegation target (reuse provider + systemPrompt)
|
|
212
|
+
delegationTargets.set(loaded.name, { appName: loaded.name, provider, systemPrompt });
|
|
213
|
+
const isMultiTenant = loaded.binding.channels.some((ch) => ch.multiTenant === true);
|
|
214
|
+
if (isMultiTenant) {
|
|
215
|
+
// Multi-tenant: use TenantRegistry + tenant routes
|
|
216
|
+
const tenantStorageDir = join(resolved.memoryBasePath, "tenants");
|
|
217
|
+
const tenantRegistry = new TenantRegistry(tenantStorageDir);
|
|
218
|
+
tenantRegistry.load();
|
|
219
|
+
loaded.tenantRuntime = {
|
|
220
|
+
appName: loaded.name,
|
|
221
|
+
orchestrator,
|
|
222
|
+
sessionRegistry,
|
|
223
|
+
tenantRegistry,
|
|
224
|
+
billing: resolved.modeBConfig.billing,
|
|
225
|
+
};
|
|
226
|
+
// WhatsApp webhook: find whatsapp channel with verifyTokenEnv
|
|
227
|
+
const whatsappChannel = loaded.binding.channels.find((ch) => ch.type === "whatsapp");
|
|
228
|
+
if (whatsappChannel) {
|
|
229
|
+
const verifyTokenEnv = whatsappChannel.verifyTokenEnv ?? "";
|
|
230
|
+
loaded.whatsappWebhookConfig = {
|
|
231
|
+
appName: loaded.name,
|
|
232
|
+
orchestrator,
|
|
233
|
+
sessionRegistry,
|
|
234
|
+
tenantRegistry,
|
|
235
|
+
verifyToken: verifyTokenEnv ? process.env[verifyTokenEnv] ?? "" : "",
|
|
236
|
+
memoryBasePath: resolved.memoryBasePath,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// Admin routes
|
|
240
|
+
const adminChannel = loaded.binding.channels.find((ch) => ch.adminTokenEnv);
|
|
241
|
+
const adminTokenEnv = adminChannel?.adminTokenEnv ?? "";
|
|
242
|
+
loaded.tenantAdminConfig = {
|
|
243
|
+
tenantRegistry,
|
|
244
|
+
appName: loaded.name,
|
|
245
|
+
adminToken: adminTokenEnv ? process.env[adminTokenEnv] ?? undefined : undefined,
|
|
246
|
+
};
|
|
247
|
+
const tenantCount = tenantRegistry.list(loaded.name).length;
|
|
248
|
+
console.log(` ${loaded.name}: multi-tenant mode (${tenantCount} tenants loaded)`);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
// Standard Mode B (non-tenant)
|
|
252
|
+
loaded.modeBRuntime = {
|
|
253
|
+
appName: loaded.name,
|
|
254
|
+
orchestrator,
|
|
255
|
+
sessionRegistry,
|
|
256
|
+
billing: resolved.modeBConfig.billing,
|
|
257
|
+
systemPrompt,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const port = options?.port ?? gatewayConfig.port;
|
|
262
|
+
const delegationRegistry = { targets: delegationTargets };
|
|
263
|
+
// Create health registry and register subsystem checkers
|
|
264
|
+
const healthRegistry = new HealthRegistry();
|
|
265
|
+
const startTime = Date.now();
|
|
266
|
+
// Register provider health checker
|
|
267
|
+
healthRegistry.register("providers", () => {
|
|
268
|
+
const providerStatuses = {};
|
|
269
|
+
let hasError = false;
|
|
270
|
+
for (const loaded of loadedApps) {
|
|
271
|
+
const resolved = resolvedApps.find((r) => r.name === loaded.name);
|
|
272
|
+
if (resolved?.modeBConfig?.runtime === "provider-adapter") {
|
|
273
|
+
const providerName = resolved.modeBConfig.provider.name;
|
|
274
|
+
// Provider is considered ok if we have an API key configured
|
|
275
|
+
const apiKeyEnv = resolved.modeBConfig.provider.apiKeyEnv;
|
|
276
|
+
const hasKey = apiKeyEnv ? Boolean(process.env[apiKeyEnv]) : false;
|
|
277
|
+
providerStatuses[providerName] = hasKey ? "ok" : "error";
|
|
278
|
+
if (!hasKey)
|
|
279
|
+
hasError = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
status: hasError ? "error" : "ok",
|
|
284
|
+
details: providerStatuses,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
// Register budget health checker
|
|
288
|
+
healthRegistry.register("budget", () => {
|
|
289
|
+
// Check if any app has billing configured
|
|
290
|
+
const hasBilling = loadedApps.some((loaded) => {
|
|
291
|
+
const resolved = resolvedApps.find((r) => r.name === loaded.name);
|
|
292
|
+
return Boolean(resolved?.modeBConfig?.billing);
|
|
293
|
+
});
|
|
294
|
+
if (!hasBilling) {
|
|
295
|
+
return { status: "ok", details: { configured: false } };
|
|
296
|
+
}
|
|
297
|
+
return { status: "ok", details: { configured: true } };
|
|
298
|
+
});
|
|
299
|
+
// Initialize trigger registry for apps with triggers
|
|
300
|
+
const triggerRegistry = new TriggerRegistry({ eventBus: gatewayEventBus });
|
|
301
|
+
for (const loaded of loadedApps) {
|
|
302
|
+
const triggers = loaded.app.triggers;
|
|
303
|
+
if (triggers && triggers.length > 0) {
|
|
304
|
+
triggerRegistry.registerApp(loaded.name, triggers);
|
|
305
|
+
console.log(` ${loaded.name}: ${triggers.length} trigger(s) registered`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Initialize safety pipelines per app
|
|
309
|
+
const safetyPipelines = new Map();
|
|
310
|
+
for (const loaded of loadedApps) {
|
|
311
|
+
if (loaded.app.safety) {
|
|
312
|
+
safetyPipelines.set(loaded.name, new SafetyPipeline(loaded.app.safety));
|
|
313
|
+
console.log(` ${loaded.name}: safety pipeline enabled`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const approvalRegistry = new ApprovalGateRegistry();
|
|
317
|
+
const devOrchestrator = options?.devMode
|
|
318
|
+
? new DevOrchestrator({ eventBus: gatewayEventBus, approvalRegistry })
|
|
319
|
+
: undefined;
|
|
320
|
+
const studioDistPath = options?.studioDistPath ?? (options?.devMode ? resolveStudioDist() : undefined);
|
|
321
|
+
// Initialize dev-mode memory stores (one per layer)
|
|
322
|
+
let devMemoryStores;
|
|
323
|
+
if (options?.devMode) {
|
|
324
|
+
const firstResolved = resolvedApps[0];
|
|
325
|
+
if (firstResolved) {
|
|
326
|
+
const devMemoryDir = join(firstResolved.memoryBasePath, "dev");
|
|
327
|
+
mkdirSync(devMemoryDir, { recursive: true });
|
|
328
|
+
devMemoryStores = new Map();
|
|
329
|
+
for (const layer of ["user", "agent", "project"]) {
|
|
330
|
+
devMemoryStores.set(layer, new SqliteMemoryStore({
|
|
331
|
+
dbPath: join(devMemoryDir, `${layer}.db`),
|
|
332
|
+
layer,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const tokenStore = options?.devMode ? new DevTokenStore() : undefined;
|
|
338
|
+
const { upgradeWebSocket, websocket: bunWebsocket } = createBunWebSocket();
|
|
339
|
+
const honoApp = createGatewayApp({
|
|
340
|
+
port,
|
|
341
|
+
apps: loadedApps,
|
|
342
|
+
delegationRegistry,
|
|
343
|
+
healthRegistry,
|
|
344
|
+
startTime,
|
|
345
|
+
triggerRegistry,
|
|
346
|
+
safetyPipelines,
|
|
347
|
+
upgradeWebSocket,
|
|
348
|
+
validateToken: tokenStore ? (token) => tokenStore.validate(token) : undefined,
|
|
349
|
+
devMode: options?.devMode,
|
|
350
|
+
studioDistPath,
|
|
351
|
+
devRoutesConfig: options?.devMode
|
|
352
|
+
? {
|
|
353
|
+
...buildSharedDevRoutesConfig({
|
|
354
|
+
eventBus: gatewayEventBus,
|
|
355
|
+
costTracker,
|
|
356
|
+
approvalRegistry,
|
|
357
|
+
devOrchestrator,
|
|
358
|
+
tokenStore,
|
|
359
|
+
}),
|
|
360
|
+
getPhaseState: () => {
|
|
361
|
+
const active = sessionRegistry.activeSessions();
|
|
362
|
+
const orch = devOrchestrator?.orchestrator;
|
|
363
|
+
return {
|
|
364
|
+
status: orch && devOrchestrator.isRunning ? orch.status : (active.length > 0 ? "active" : "idle"),
|
|
365
|
+
activeSessions: active.length,
|
|
366
|
+
sessions: active.map((s) => ({
|
|
367
|
+
id: s.id,
|
|
368
|
+
appName: s.appName,
|
|
369
|
+
userId: s.userId,
|
|
370
|
+
messageCount: s.messageCount,
|
|
371
|
+
createdAt: s.createdAt.toISOString(),
|
|
372
|
+
lastActivityAt: s.lastActivityAt.toISOString(),
|
|
373
|
+
})),
|
|
374
|
+
orchestrator: orch ? {
|
|
375
|
+
sessionId: orch.sessionId,
|
|
376
|
+
status: orch.status,
|
|
377
|
+
phase: orch.currentPhase,
|
|
378
|
+
task: orch.task,
|
|
379
|
+
} : undefined,
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
getMemorySnapshot: () => {
|
|
383
|
+
if (!devMemoryStores)
|
|
384
|
+
return { entries: [] };
|
|
385
|
+
const counts = {};
|
|
386
|
+
for (const [layer, store] of devMemoryStores) {
|
|
387
|
+
counts[layer] = store.count;
|
|
388
|
+
}
|
|
389
|
+
return { layers: counts, total: Object.values(counts).reduce((a, b) => a + b, 0) };
|
|
390
|
+
},
|
|
391
|
+
getAppNames: () => loadedApps.map((a) => a.name),
|
|
392
|
+
getSafetyMetrics: () => {
|
|
393
|
+
if (safetyPipelines.size === 0)
|
|
394
|
+
return { enabled: false };
|
|
395
|
+
const apps = {};
|
|
396
|
+
for (const [appName, pipeline] of safetyPipelines) {
|
|
397
|
+
apps[appName] = pipeline.metrics;
|
|
398
|
+
}
|
|
399
|
+
return { enabled: true, apps };
|
|
400
|
+
},
|
|
401
|
+
getTriggers: () => triggerRegistry.listAll(),
|
|
402
|
+
getMemoryByScope: async (scope, q, tags) => {
|
|
403
|
+
if (!devMemoryStores)
|
|
404
|
+
return [];
|
|
405
|
+
const validLayers = ["user", "agent", "project"];
|
|
406
|
+
const layer = validLayers.includes(scope) ? scope : "project";
|
|
407
|
+
const store = devMemoryStores.get(layer);
|
|
408
|
+
if (!store)
|
|
409
|
+
return [];
|
|
410
|
+
if (q) {
|
|
411
|
+
const results = await store.search(q, 100);
|
|
412
|
+
return results.map((r) => ({
|
|
413
|
+
id: r.entry.id,
|
|
414
|
+
layer: r.entry.layer,
|
|
415
|
+
content: r.entry.content,
|
|
416
|
+
tags: r.entry.tags,
|
|
417
|
+
score: r.score,
|
|
418
|
+
snippet: r.snippet,
|
|
419
|
+
createdAt: r.entry.createdAt.toISOString(),
|
|
420
|
+
lastAccessedAt: r.entry.lastAccessedAt.toISOString(),
|
|
421
|
+
accessCount: r.entry.accessCount,
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
const entries = store.listEntries({ tags });
|
|
425
|
+
return entries.map((e) => ({
|
|
426
|
+
id: e.id,
|
|
427
|
+
layer: e.layer,
|
|
428
|
+
content: e.content,
|
|
429
|
+
tags: e.tags,
|
|
430
|
+
createdAt: e.createdAt.toISOString(),
|
|
431
|
+
lastAccessedAt: e.lastAccessedAt.toISOString(),
|
|
432
|
+
accessCount: e.accessCount,
|
|
433
|
+
}));
|
|
434
|
+
},
|
|
435
|
+
createMemoryEntry: async (entry) => {
|
|
436
|
+
if (!devMemoryStores)
|
|
437
|
+
return { id: "" };
|
|
438
|
+
const validLayers = ["user", "agent", "project"];
|
|
439
|
+
const layer = validLayers.includes(entry["layer"])
|
|
440
|
+
? entry["layer"]
|
|
441
|
+
: "project";
|
|
442
|
+
const store = devMemoryStores.get(layer);
|
|
443
|
+
if (!store)
|
|
444
|
+
return { id: "" };
|
|
445
|
+
const content = typeof entry["content"] === "string" ? entry["content"] : String(entry["content"] ?? "");
|
|
446
|
+
const rawTags = Array.isArray(entry["tags"]) ? entry["tags"] : [];
|
|
447
|
+
const entryTags = rawTags.filter((t) => typeof t === "string");
|
|
448
|
+
const id = await store.save({ layer, content, tags: entryTags });
|
|
449
|
+
return { id };
|
|
450
|
+
},
|
|
451
|
+
deleteMemoryEntry: async (id) => {
|
|
452
|
+
if (!devMemoryStores)
|
|
453
|
+
return false;
|
|
454
|
+
for (const store of devMemoryStores.values()) {
|
|
455
|
+
if (store.hasEntry(id)) {
|
|
456
|
+
await store.forget(id);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
},
|
|
462
|
+
getAppGraph: () => {
|
|
463
|
+
const firstLoaded = loadedApps[0];
|
|
464
|
+
if (!firstLoaded)
|
|
465
|
+
return undefined;
|
|
466
|
+
return appToGraph(firstLoaded.app);
|
|
467
|
+
},
|
|
468
|
+
getYamlContent: () => {
|
|
469
|
+
try {
|
|
470
|
+
const firstBinding = gatewayConfig.apps[0];
|
|
471
|
+
if (!firstBinding)
|
|
472
|
+
return undefined;
|
|
473
|
+
const yamlPath = join(gatewayYamlDir, firstBinding.config);
|
|
474
|
+
return readFileSync(yamlPath, "utf-8");
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
putYamlContent: (content) => {
|
|
481
|
+
try {
|
|
482
|
+
// Validate YAML before writing
|
|
483
|
+
parseAppYaml(content);
|
|
484
|
+
const firstBinding = gatewayConfig.apps[0];
|
|
485
|
+
if (!firstBinding)
|
|
486
|
+
return { ok: false, errors: ["No YAML path available"] };
|
|
487
|
+
const yamlPath = join(gatewayYamlDir, firstBinding.config);
|
|
488
|
+
writeFileSync(yamlPath, content, "utf-8");
|
|
489
|
+
return { ok: true };
|
|
490
|
+
}
|
|
491
|
+
catch (e) {
|
|
492
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
493
|
+
return { ok: false, errors: [msg] };
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
getEvalExperiments: () => {
|
|
497
|
+
const firstLoaded = loadedApps[0];
|
|
498
|
+
if (!firstLoaded?.app.eval?.experiments)
|
|
499
|
+
return [];
|
|
500
|
+
return firstLoaded.app.eval.experiments.map((exp) => ({
|
|
501
|
+
name: exp.name,
|
|
502
|
+
dataset: exp.dataset,
|
|
503
|
+
scorers: [...exp.scorers],
|
|
504
|
+
}));
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
: undefined,
|
|
508
|
+
});
|
|
509
|
+
if (studioDistPath) {
|
|
510
|
+
mountStudio(honoApp, studioDistPath);
|
|
511
|
+
}
|
|
512
|
+
triggerRegistry.start();
|
|
513
|
+
const appNames = loadedApps.map((a) => a.name).join(", ");
|
|
514
|
+
console.log(`Gateway started on port ${port} with ${loadedApps.length} apps: ${appNames}`);
|
|
515
|
+
if (options?.devMode) {
|
|
516
|
+
console.log(`Studio: http://localhost:${port}/${studioDistPath ? "studio" : "dev"}/`);
|
|
517
|
+
}
|
|
518
|
+
await serveAndWait(honoApp, port, () => triggerRegistry.stop(), bunWebsocket);
|
|
519
|
+
}
|
|
520
|
+
/** Create a ProviderAdapter from a Mode B provider config */
|
|
521
|
+
function createProviderFromConfig(config) {
|
|
522
|
+
const apiKey = config.apiKeyEnv ? process.env[config.apiKeyEnv] ?? "" : "";
|
|
523
|
+
const model = config.model;
|
|
524
|
+
switch (config.name) {
|
|
525
|
+
case "anthropic":
|
|
526
|
+
return new AnthropicAdapter({ apiKey, defaultModel: model });
|
|
527
|
+
case "openai":
|
|
528
|
+
return new OpenAIAdapter({ apiKey, defaultModel: model });
|
|
529
|
+
case "deepseek":
|
|
530
|
+
return new DeepSeekAdapter({ apiKey, defaultModel: model });
|
|
531
|
+
case "ollama":
|
|
532
|
+
return new OllamaAdapter({ defaultModel: model });
|
|
533
|
+
default:
|
|
534
|
+
throw new KilnError("CONFIG_INVALID", `Unknown provider: ${config.name}`, {
|
|
535
|
+
context: { provider: config.name },
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/** Build a system prompt from an App composite using agent metadata */
|
|
540
|
+
function buildSystemPromptFromApp(app) {
|
|
541
|
+
// Find the primary agent: the manager of the fallback team, or first agent
|
|
542
|
+
const fallbackTeam = app.teams[app.router.fallback];
|
|
543
|
+
let primaryAgent;
|
|
544
|
+
if (fallbackTeam) {
|
|
545
|
+
if (fallbackTeam.manager && fallbackTeam.agents[fallbackTeam.manager]) {
|
|
546
|
+
primaryAgent = fallbackTeam.agents[fallbackTeam.manager];
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
const agents = Object.values(fallbackTeam.agents);
|
|
550
|
+
primaryAgent = agents[0];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (!primaryAgent) {
|
|
554
|
+
return `You are ${app.name}. Respond helpfully.`;
|
|
555
|
+
}
|
|
556
|
+
const parts = [];
|
|
557
|
+
// Identity
|
|
558
|
+
if (primaryAgent.backstory) {
|
|
559
|
+
parts.push(primaryAgent.backstory);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
parts.push(`You are ${primaryAgent.name}, ${primaryAgent.role}.`);
|
|
563
|
+
}
|
|
564
|
+
// Goal
|
|
565
|
+
if (primaryAgent.goal) {
|
|
566
|
+
parts.push(`\nYour goal: ${primaryAgent.goal}`);
|
|
567
|
+
}
|
|
568
|
+
// Instructions
|
|
569
|
+
if (primaryAgent.instructions) {
|
|
570
|
+
parts.push(`\n${primaryAgent.instructions}`);
|
|
571
|
+
}
|
|
572
|
+
return parts.join("\n");
|
|
573
|
+
}
|
|
574
|
+
function appToGraph(app) {
|
|
575
|
+
return {
|
|
576
|
+
name: app.name,
|
|
577
|
+
teams: Object.entries(app.teams).map(([name, team]) => ({
|
|
578
|
+
name,
|
|
579
|
+
agents: Object.values(team.agents).map((a) => ({
|
|
580
|
+
name: a.name,
|
|
581
|
+
role: a.role,
|
|
582
|
+
goal: a.goal,
|
|
583
|
+
tier: a.tier,
|
|
584
|
+
tools: [...a.tools],
|
|
585
|
+
modalities: a.modalities ? [...a.modalities] : undefined,
|
|
586
|
+
})),
|
|
587
|
+
capabilities: team.capabilities.map((c) => c.name),
|
|
588
|
+
phases: [...team.workflow.phases],
|
|
589
|
+
mode: team.mode,
|
|
590
|
+
})),
|
|
591
|
+
router: {
|
|
592
|
+
rules: app.router.rules.map((r) => ({ pattern: r.match, team: r.team })),
|
|
593
|
+
fallback: app.router.fallback,
|
|
594
|
+
classifier: app.router.classifier?.name,
|
|
595
|
+
},
|
|
596
|
+
channels: [...app.channels],
|
|
597
|
+
triggers: app.triggers?.map((t) => t.name) ?? [],
|
|
598
|
+
hasKnowledge: !!app.knowledge,
|
|
599
|
+
hasEval: !!app.eval,
|
|
600
|
+
hasSafety: !!app.safety,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function mountStudio(app, distPath) {
|
|
604
|
+
app.get("/studio", (c) => c.redirect("/studio/"));
|
|
605
|
+
app.use("/studio/*", serveStatic({
|
|
606
|
+
root: distPath,
|
|
607
|
+
rewriteRequestPath: (path) => {
|
|
608
|
+
const stripped = path.replace(/^\/studio/, "");
|
|
609
|
+
return stripped === "/" || stripped === "" ? "/index.html" : stripped;
|
|
610
|
+
},
|
|
611
|
+
}));
|
|
612
|
+
app.get("/studio/*", (c) => {
|
|
613
|
+
const html = readFileSync(join(distPath, "index.html"), "utf-8");
|
|
614
|
+
return c.html(html);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
async function serveAndWait(app, port, onShutdown, websocketHandler) {
|
|
618
|
+
const websocket = websocketHandler ?? createBunWebSocket().websocket;
|
|
619
|
+
let server;
|
|
620
|
+
try {
|
|
621
|
+
server = Bun.serve({ port, fetch: app.fetch, websocket });
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
if (err instanceof Error && "code" in err && err.code === "EADDRINUSE") {
|
|
625
|
+
console.error(`Error: Port ${port} is already in use.`);
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
throw err;
|
|
629
|
+
}
|
|
630
|
+
await new Promise((resolve) => {
|
|
631
|
+
const shutdown = () => {
|
|
632
|
+
console.log("\nShutting down...");
|
|
633
|
+
onShutdown?.();
|
|
634
|
+
server.stop(true);
|
|
635
|
+
resolve();
|
|
636
|
+
};
|
|
637
|
+
process.on("SIGINT", shutdown);
|
|
638
|
+
process.on("SIGTERM", shutdown);
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
export async function startDevServer(options) {
|
|
642
|
+
const port = options?.port ?? 4800;
|
|
643
|
+
const eventBus = new EventBus(100);
|
|
644
|
+
const costTracker = new CostTracker();
|
|
645
|
+
const studioDistPath = options?.studioDistPath ?? resolveStudioDist();
|
|
646
|
+
let app;
|
|
647
|
+
const appYamlPath = options?.appYamlPath;
|
|
648
|
+
if (appYamlPath && existsSync(appYamlPath)) {
|
|
649
|
+
try {
|
|
650
|
+
app = parseAppYaml(readFileSync(appYamlPath, "utf-8"));
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
// Invalid YAML -- Studio will show empty graph
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const approvalRegistry = new ApprovalGateRegistry();
|
|
657
|
+
const devOrchestrator = new DevOrchestrator({ eventBus, approvalRegistry });
|
|
658
|
+
const tokenStore = new DevTokenStore();
|
|
659
|
+
const honoApp = createGatewayApp({
|
|
660
|
+
port,
|
|
661
|
+
apps: [],
|
|
662
|
+
devMode: true,
|
|
663
|
+
studioDistPath,
|
|
664
|
+
upgradeWebSocket: undefined,
|
|
665
|
+
validateToken: (token) => tokenStore.validate(token),
|
|
666
|
+
devRoutesConfig: {
|
|
667
|
+
...buildSharedDevRoutesConfig({
|
|
668
|
+
eventBus,
|
|
669
|
+
costTracker,
|
|
670
|
+
approvalRegistry,
|
|
671
|
+
devOrchestrator,
|
|
672
|
+
tokenStore,
|
|
673
|
+
}),
|
|
674
|
+
getPhaseState: () => {
|
|
675
|
+
const orch = devOrchestrator.orchestrator;
|
|
676
|
+
return {
|
|
677
|
+
status: devOrchestrator.isRunning ? orch.status : "idle",
|
|
678
|
+
orchestrator: {
|
|
679
|
+
sessionId: orch.sessionId,
|
|
680
|
+
status: orch.status,
|
|
681
|
+
phase: orch.currentPhase,
|
|
682
|
+
task: orch.task,
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
},
|
|
686
|
+
getMemorySnapshot: () => ({ entries: [] }),
|
|
687
|
+
getSafetyMetrics: () => ({ enabled: false }),
|
|
688
|
+
getAppNames: () => app ? [app.name] : [],
|
|
689
|
+
getTriggers: () => [],
|
|
690
|
+
getAppGraph: () => app ? appToGraph(app) : undefined,
|
|
691
|
+
getYamlContent: () => {
|
|
692
|
+
if (!appYamlPath)
|
|
693
|
+
return undefined;
|
|
694
|
+
try {
|
|
695
|
+
return readFileSync(appYamlPath, "utf-8");
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
putYamlContent: (content) => {
|
|
702
|
+
if (!appYamlPath)
|
|
703
|
+
return { ok: false, errors: ["No app.yaml path"] };
|
|
704
|
+
try {
|
|
705
|
+
parseAppYaml(content);
|
|
706
|
+
writeFileSync(appYamlPath, content, "utf-8");
|
|
707
|
+
try {
|
|
708
|
+
app = parseAppYaml(content);
|
|
709
|
+
}
|
|
710
|
+
catch { /* keep previous */ }
|
|
711
|
+
return { ok: true };
|
|
712
|
+
}
|
|
713
|
+
catch (e) {
|
|
714
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
715
|
+
return { ok: false, errors: [msg] };
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
getEvalExperiments: () => {
|
|
719
|
+
if (!app?.eval?.experiments)
|
|
720
|
+
return [];
|
|
721
|
+
return app.eval.experiments.map((exp) => ({
|
|
722
|
+
name: exp.name,
|
|
723
|
+
dataset: exp.dataset,
|
|
724
|
+
scorers: [...exp.scorers],
|
|
725
|
+
}));
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
});
|
|
729
|
+
if (studioDistPath) {
|
|
730
|
+
mountStudio(honoApp, studioDistPath);
|
|
731
|
+
}
|
|
732
|
+
console.log(`Dev server started on port ${port}`);
|
|
733
|
+
console.log(`Studio: http://localhost:${port}/${studioDistPath ? "studio" : "dev"}/`);
|
|
734
|
+
await serveAndWait(honoApp, port);
|
|
735
|
+
}
|
|
736
|
+
//# sourceMappingURL=gateway-server.js.map
|