@delexec/ops 0.1.0
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/README.md +3 -0
- package/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller/README.md +3 -0
- package/node_modules/@delexec/caller-controller/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller/package.json +53 -0
- package/node_modules/@delexec/caller-controller/src/server.js +127 -0
- package/node_modules/@delexec/caller-controller-core/README.md +3 -0
- package/node_modules/@delexec/caller-controller-core/README.zh-CN.md +6 -0
- package/node_modules/@delexec/caller-controller-core/package.json +26 -0
- package/node_modules/@delexec/caller-controller-core/src/index.js +1612 -0
- package/node_modules/@delexec/caller-skill-adapter/package.json +12 -0
- package/node_modules/@delexec/caller-skill-adapter/src/server.js +1042 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/README.md +65 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/package.json +16 -0
- package/node_modules/@delexec/caller-skill-mcp-adapter/src/server.js +527 -0
- package/node_modules/@delexec/responder-controller/README.md +3 -0
- package/node_modules/@delexec/responder-controller/README.zh-CN.md +6 -0
- package/node_modules/@delexec/responder-controller/package.json +53 -0
- package/node_modules/@delexec/responder-controller/src/server.js +254 -0
- package/node_modules/@delexec/responder-runtime-core/README.md +3 -0
- package/node_modules/@delexec/responder-runtime-core/README.zh-CN.md +6 -0
- package/node_modules/@delexec/responder-runtime-core/package.json +26 -0
- package/node_modules/@delexec/responder-runtime-core/src/executors.js +326 -0
- package/node_modules/@delexec/responder-runtime-core/src/index.js +1202 -0
- package/node_modules/@delexec/runtime-utils/README.md +3 -0
- package/node_modules/@delexec/runtime-utils/README.zh-CN.md +6 -0
- package/node_modules/@delexec/runtime-utils/package.json +23 -0
- package/node_modules/@delexec/runtime-utils/src/index.js +338 -0
- package/node_modules/@delexec/sqlite-store/README.md +3 -0
- package/node_modules/@delexec/sqlite-store/README.zh-CN.md +6 -0
- package/node_modules/@delexec/sqlite-store/package.json +26 -0
- package/node_modules/@delexec/sqlite-store/src/index.js +68 -0
- package/node_modules/@delexec/transport-email/README.md +3 -0
- package/node_modules/@delexec/transport-email/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-email/package.json +23 -0
- package/node_modules/@delexec/transport-email/src/index.js +185 -0
- package/node_modules/@delexec/transport-emailengine/README.md +3 -0
- package/node_modules/@delexec/transport-emailengine/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-emailengine/package.json +26 -0
- package/node_modules/@delexec/transport-emailengine/src/index.js +210 -0
- package/node_modules/@delexec/transport-gmail/README.md +3 -0
- package/node_modules/@delexec/transport-gmail/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-gmail/package.json +26 -0
- package/node_modules/@delexec/transport-gmail/src/index.js +295 -0
- package/node_modules/@delexec/transport-relay-http/README.md +3 -0
- package/node_modules/@delexec/transport-relay-http/README.zh-CN.md +6 -0
- package/node_modules/@delexec/transport-relay-http/package.json +23 -0
- package/node_modules/@delexec/transport-relay-http/src/index.js +124 -0
- package/package.json +64 -0
- package/src/cli.js +1571 -0
- package/src/config.js +1180 -0
- package/src/example-hotline-worker.js +65 -0
- package/src/example-hotline.js +196 -0
- package/src/logging.js +56 -0
- package/src/supervisor.js +3070 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createHotlineRouterExecutor,
|
|
7
|
+
createResponderControllerServer,
|
|
8
|
+
createResponderState,
|
|
9
|
+
hydrateResponderState,
|
|
10
|
+
serializeResponderState,
|
|
11
|
+
startResponderHeartbeatLoop
|
|
12
|
+
} from "@delexec/responder-runtime-core";
|
|
13
|
+
import { createSqliteSnapshotStore } from "@delexec/sqlite-store";
|
|
14
|
+
import { createEmailEngineTransportAdapter } from "@delexec/transport-emailengine";
|
|
15
|
+
import { createGmailTransportAdapter } from "@delexec/transport-gmail";
|
|
16
|
+
import { createRelayHttpTransportAdapter } from "@delexec/transport-relay-http";
|
|
17
|
+
import { buildOpsEnvSearchPaths, getOpsConfigFile, getResponderConfigFile, loadEnvFiles, readJsonFile } from "@delexec/runtime-utils";
|
|
18
|
+
|
|
19
|
+
export * from "@delexec/responder-runtime-core";
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
const ROOT_DIR = path.resolve(__dirname, "../../..");
|
|
24
|
+
|
|
25
|
+
loadEnvFiles(buildOpsEnvSearchPaths(ROOT_DIR, "responder"));
|
|
26
|
+
|
|
27
|
+
function isDirectRun() {
|
|
28
|
+
if (!process.argv[1]) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return fs.realpathSync.native(path.resolve(process.argv[1])) === fs.realpathSync.native(__filename);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodePemEnv(value) {
|
|
35
|
+
if (!value) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return value.replace(/\\n/g, "\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function loadResponderStateFromEnv() {
|
|
42
|
+
const responderId = process.env.RESPONDER_ID || null;
|
|
43
|
+
const hotlineIds = (process.env.HOTLINE_IDS || "")
|
|
44
|
+
.split(",")
|
|
45
|
+
.map((item) => item.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
const publicKeyPem = decodePemEnv(process.env.RESPONDER_SIGNING_PUBLIC_KEY_PEM);
|
|
48
|
+
const privateKeyPem = decodePemEnv(process.env.RESPONDER_SIGNING_PRIVATE_KEY_PEM);
|
|
49
|
+
|
|
50
|
+
if (!responderId && hotlineIds.length === 0 && !publicKeyPem && !privateKeyPem) {
|
|
51
|
+
return createResponderState();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stateOptions = {};
|
|
55
|
+
if (responderId) {
|
|
56
|
+
stateOptions.responderId = responderId;
|
|
57
|
+
}
|
|
58
|
+
if (hotlineIds.length > 0) {
|
|
59
|
+
stateOptions.hotlineIds = hotlineIds;
|
|
60
|
+
}
|
|
61
|
+
if (publicKeyPem || privateKeyPem) {
|
|
62
|
+
if (!publicKeyPem || !privateKeyPem) {
|
|
63
|
+
throw new Error("responder_signing_key_pair_incomplete");
|
|
64
|
+
}
|
|
65
|
+
stateOptions.signing = {
|
|
66
|
+
publicKeyPem,
|
|
67
|
+
privateKeyPem
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return createResponderState(stateOptions);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function loadResponderConfigFromDisk() {
|
|
75
|
+
const opsConfig = readJsonFile(getOpsConfigFile(), null);
|
|
76
|
+
if (opsConfig?.responder) {
|
|
77
|
+
return {
|
|
78
|
+
responder_id: opsConfig.responder.responder_id || null,
|
|
79
|
+
display_name: opsConfig.responder.display_name || null,
|
|
80
|
+
enabled: opsConfig.responder.enabled !== false,
|
|
81
|
+
hotlines: Array.isArray(opsConfig.responder.hotlines) ? opsConfig.responder.hotlines : []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return readJsonFile(getResponderConfigFile(), { responder_id: null, display_name: null, enabled: true, hotlines: [] });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mergeConfigHotlinesIntoState(state, config) {
|
|
88
|
+
const configuredIds = Array.isArray(config?.hotlines)
|
|
89
|
+
? config.hotlines.map((item) => item?.hotline_id).filter(Boolean)
|
|
90
|
+
: [];
|
|
91
|
+
if (configuredIds.length === 0) {
|
|
92
|
+
return state;
|
|
93
|
+
}
|
|
94
|
+
const merged = new Set([...(state.identity.hotline_ids || []), ...configuredIds]);
|
|
95
|
+
state.identity.hotline_ids = Array.from(merged);
|
|
96
|
+
if (!state.identity.responder_id && config?.responder_id) {
|
|
97
|
+
state.identity.responder_id = config.responder_id;
|
|
98
|
+
}
|
|
99
|
+
state.hotlines = Array.isArray(config?.hotlines) ? config.hotlines : [];
|
|
100
|
+
return state;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createExecutorFromConfig(config) {
|
|
104
|
+
const hotlines = Array.isArray(config?.hotlines) ? config.hotlines : [];
|
|
105
|
+
if (hotlines.length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return createHotlineRouterExecutor(hotlines);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadPlatformConfigFromEnv() {
|
|
112
|
+
const baseUrl = process.env.PLATFORM_API_BASE_URL || null;
|
|
113
|
+
if (!baseUrl) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
baseUrl,
|
|
119
|
+
apiKey: process.env.RESPONDER_PLATFORM_API_KEY || process.env.PLATFORM_API_KEY || null,
|
|
120
|
+
responderId: process.env.RESPONDER_ID || null
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function loadResponderGuardrailsFromEnv() {
|
|
125
|
+
const allowedTaskTypes = (process.env.RESPONDER_ALLOWED_TASK_TYPES || "")
|
|
126
|
+
.split(",")
|
|
127
|
+
.map((item) => item.trim())
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
const maxHardTimeoutS = process.env.RESPONDER_MAX_HARD_TIMEOUT_S || null;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
maxHardTimeoutS: maxHardTimeoutS ? Number(maxHardTimeoutS) : null,
|
|
133
|
+
allowedTaskTypes: allowedTaskTypes.length > 0 ? allowedTaskTypes : null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadTransportConfigFromEnv() {
|
|
138
|
+
const transportType = process.env.TRANSPORT_TYPE || (process.env.TRANSPORT_BASE_URL ? "relay_http" : null);
|
|
139
|
+
if (transportType === "email") {
|
|
140
|
+
const provider = process.env.TRANSPORT_EMAIL_PROVIDER || process.env.TRANSPORT_PROVIDER || "unknown";
|
|
141
|
+
if (provider === "emailengine") {
|
|
142
|
+
return createEmailEngineTransportAdapter({
|
|
143
|
+
baseUrl: process.env.TRANSPORT_EMAILENGINE_BASE_URL,
|
|
144
|
+
account: process.env.TRANSPORT_EMAILENGINE_ACCOUNT,
|
|
145
|
+
accessToken: process.env.TRANSPORT_EMAILENGINE_ACCESS_TOKEN,
|
|
146
|
+
sender: process.env.TRANSPORT_EMAIL_SENDER || process.env.TRANSPORT_EMAILENGINE_ACCOUNT || null,
|
|
147
|
+
receiver: process.env.TRANSPORT_EMAIL_RECEIVER || process.env.RESPONDER_ID || null
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (provider === "gmail") {
|
|
151
|
+
return createGmailTransportAdapter({
|
|
152
|
+
clientId: process.env.TRANSPORT_GMAIL_CLIENT_ID,
|
|
153
|
+
clientSecret: process.env.TRANSPORT_GMAIL_CLIENT_SECRET,
|
|
154
|
+
refreshToken: process.env.TRANSPORT_GMAIL_REFRESH_TOKEN,
|
|
155
|
+
user: process.env.TRANSPORT_GMAIL_USER,
|
|
156
|
+
sender: process.env.TRANSPORT_EMAIL_SENDER || process.env.TRANSPORT_GMAIL_USER || null,
|
|
157
|
+
receiver: process.env.TRANSPORT_EMAIL_RECEIVER || process.env.RESPONDER_ID || null
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`TRANSPORT_NOT_IMPLEMENTED: email transport provider ${provider} is not implemented yet`);
|
|
161
|
+
}
|
|
162
|
+
const baseUrl = process.env.TRANSPORT_BASE_URL || null;
|
|
163
|
+
if (!baseUrl || !transportType) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return createRelayHttpTransportAdapter({
|
|
168
|
+
baseUrl,
|
|
169
|
+
receiver: process.env.TRANSPORT_RECEIVER || process.env.RESPONDER_ID || "responder-controller"
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function createOptionalPersistence(serviceName) {
|
|
174
|
+
const sqlitePath = process.env.SQLITE_DATABASE_PATH || null;
|
|
175
|
+
if (!sqlitePath) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const store = await createSqliteSnapshotStore({
|
|
180
|
+
databasePath: sqlitePath,
|
|
181
|
+
serviceName
|
|
182
|
+
});
|
|
183
|
+
await store.migrate();
|
|
184
|
+
return store;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (isDirectRun()) {
|
|
188
|
+
const port = Number(process.env.PORT || 8082);
|
|
189
|
+
const serviceName = process.env.SERVICE_NAME || "responder-controller";
|
|
190
|
+
const responderConfig = loadResponderConfigFromDisk();
|
|
191
|
+
const state = mergeConfigHotlinesIntoState(loadResponderStateFromEnv(), responderConfig);
|
|
192
|
+
const platform = loadPlatformConfigFromEnv();
|
|
193
|
+
const transport = loadTransportConfigFromEnv();
|
|
194
|
+
const executor = createExecutorFromConfig(responderConfig);
|
|
195
|
+
const persistence = await createOptionalPersistence(serviceName);
|
|
196
|
+
if (persistence) {
|
|
197
|
+
hydrateResponderState(state, await persistence.loadSnapshot());
|
|
198
|
+
}
|
|
199
|
+
let stopHeartbeat = () => {};
|
|
200
|
+
const persistSnapshot = persistence
|
|
201
|
+
? async (currentState) => {
|
|
202
|
+
await persistence.saveSnapshot(serializeResponderState(currentState));
|
|
203
|
+
}
|
|
204
|
+
: null;
|
|
205
|
+
|
|
206
|
+
function restartHeartbeatLoop() {
|
|
207
|
+
stopHeartbeat();
|
|
208
|
+
if (platform?.baseUrl && platform?.apiKey) {
|
|
209
|
+
stopHeartbeat = startResponderHeartbeatLoop({
|
|
210
|
+
state,
|
|
211
|
+
platform,
|
|
212
|
+
intervalMs: Number(process.env.RESPONDER_HEARTBEAT_INTERVAL_MS || 30000),
|
|
213
|
+
onStateChanged: persistSnapshot
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
stopHeartbeat = () => {};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const server = createResponderControllerServer({
|
|
221
|
+
serviceName,
|
|
222
|
+
state,
|
|
223
|
+
transport,
|
|
224
|
+
platform,
|
|
225
|
+
...(executor ? { executor } : {}),
|
|
226
|
+
guardrails: loadResponderGuardrailsFromEnv(),
|
|
227
|
+
background: {
|
|
228
|
+
enabled: Boolean(transport),
|
|
229
|
+
receiver: process.env.TRANSPORT_RECEIVER || state.identity.responder_id,
|
|
230
|
+
inboxPollIntervalMs: Number(process.env.RESPONDER_INBOX_POLL_INTERVAL_MS || 250),
|
|
231
|
+
workerConcurrency: Number(process.env.RESPONDER_WORKER_CONCURRENCY || state.workerConcurrency || 1)
|
|
232
|
+
},
|
|
233
|
+
onStateChanged: persistSnapshot,
|
|
234
|
+
onPlatformConfigured: async () => {
|
|
235
|
+
restartHeartbeatLoop();
|
|
236
|
+
if (persistSnapshot) {
|
|
237
|
+
await persistSnapshot(state);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
server.listen(port, "0.0.0.0", () => {
|
|
243
|
+
restartHeartbeatLoop();
|
|
244
|
+
console.log(`[${serviceName}] listening on ${port}`);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
server.on("close", () => {
|
|
248
|
+
stopHeartbeat();
|
|
249
|
+
if (persistence) {
|
|
250
|
+
void persistence.saveSnapshot(serializeResponderState(state));
|
|
251
|
+
void persistence.close();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delexec/responder-runtime-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Responder runtime core logic for delegated execution",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"delegated-execution",
|
|
20
|
+
"responder",
|
|
21
|
+
"runtime"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@delexec/contracts": "^0.1.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function deferTask(reason = "deferred") {
|
|
4
|
+
return {
|
|
5
|
+
deferred: true,
|
|
6
|
+
reason
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeExecutionResult(result) {
|
|
11
|
+
if (!result || typeof result !== "object") {
|
|
12
|
+
return {
|
|
13
|
+
status: "error",
|
|
14
|
+
error: {
|
|
15
|
+
code: "HOTLINE_INVALID_RESULT",
|
|
16
|
+
message: "hotline returned an invalid result payload",
|
|
17
|
+
retryable: false
|
|
18
|
+
},
|
|
19
|
+
schema_valid: true,
|
|
20
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (result.status === "ok" || result.status === "error" || result.deferred === true) {
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
status: "ok",
|
|
30
|
+
output: result,
|
|
31
|
+
schema_valid: true,
|
|
32
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createFunctionExecutor(fn, { name = "function-executor", allowedTaskTypes = null } = {}) {
|
|
37
|
+
if (typeof fn !== "function") {
|
|
38
|
+
throw new TypeError("responder_executor_fn_required");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
allowedTaskTypes: Array.isArray(allowedTaskTypes) ? [...allowedTaskTypes] : null,
|
|
44
|
+
async execute(context) {
|
|
45
|
+
return normalizeExecutionResult(await fn(context));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function runProcessAdapter(adapter, context) {
|
|
51
|
+
if (!adapter?.cmd) {
|
|
52
|
+
throw new Error("process_adapter_cmd_required");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const child = spawn(adapter.cmd, {
|
|
57
|
+
cwd: adapter.cwd || process.cwd(),
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
...(adapter.env || {})
|
|
61
|
+
},
|
|
62
|
+
shell: true,
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let stdout = "";
|
|
67
|
+
let stderr = "";
|
|
68
|
+
|
|
69
|
+
child.stdout.on("data", (chunk) => {
|
|
70
|
+
stdout += chunk.toString("utf8");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
child.stderr.on("data", (chunk) => {
|
|
74
|
+
stderr += chunk.toString("utf8");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
child.on("error", reject);
|
|
78
|
+
|
|
79
|
+
child.on("close", (code) => {
|
|
80
|
+
if (code !== 0) {
|
|
81
|
+
resolve({
|
|
82
|
+
status: "error",
|
|
83
|
+
error: {
|
|
84
|
+
code: "HOTLINE_PROCESS_EXITED",
|
|
85
|
+
message: stderr.trim() || `process exited with code ${code}`,
|
|
86
|
+
retryable: false
|
|
87
|
+
},
|
|
88
|
+
schema_valid: true,
|
|
89
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const parsed = stdout.trim() ? JSON.parse(stdout) : null;
|
|
96
|
+
resolve(normalizeExecutionResult(parsed));
|
|
97
|
+
} catch {
|
|
98
|
+
resolve({
|
|
99
|
+
status: "error",
|
|
100
|
+
error: {
|
|
101
|
+
code: "HOTLINE_PROCESS_INVALID_JSON",
|
|
102
|
+
message: "process adapter must emit a single JSON payload on stdout",
|
|
103
|
+
retryable: false
|
|
104
|
+
},
|
|
105
|
+
schema_valid: true,
|
|
106
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
child.stdin.write(
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
request_id: context.requestId,
|
|
114
|
+
responder_id: context.responderId,
|
|
115
|
+
hotline_id: context.hotlineId,
|
|
116
|
+
task_type: context.taskType,
|
|
117
|
+
input: context.taskInput,
|
|
118
|
+
payload: context.payload,
|
|
119
|
+
constraints: context.constraints,
|
|
120
|
+
task: context.task
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
child.stdin.end();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function runHttpAdapter(adapter, context) {
|
|
128
|
+
if (!adapter?.url) {
|
|
129
|
+
throw new Error("http_adapter_url_required");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const response = await fetch(adapter.url, {
|
|
133
|
+
method: adapter.method || "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
"content-type": "application/json; charset=utf-8",
|
|
136
|
+
...(adapter.headers || {})
|
|
137
|
+
},
|
|
138
|
+
body: JSON.stringify({
|
|
139
|
+
request_id: context.requestId,
|
|
140
|
+
responder_id: context.responderId,
|
|
141
|
+
hotline_id: context.hotlineId,
|
|
142
|
+
task_type: context.taskType,
|
|
143
|
+
input: context.taskInput,
|
|
144
|
+
payload: context.payload,
|
|
145
|
+
constraints: context.constraints,
|
|
146
|
+
task: context.task
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const text = await response.text();
|
|
151
|
+
let body = null;
|
|
152
|
+
try {
|
|
153
|
+
body = text ? JSON.parse(text) : null;
|
|
154
|
+
} catch {
|
|
155
|
+
return {
|
|
156
|
+
status: "error",
|
|
157
|
+
error: {
|
|
158
|
+
code: "HOTLINE_HTTP_INVALID_JSON",
|
|
159
|
+
message: "http adapter must return JSON",
|
|
160
|
+
retryable: false
|
|
161
|
+
},
|
|
162
|
+
schema_valid: true,
|
|
163
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
return {
|
|
169
|
+
status: "error",
|
|
170
|
+
error: {
|
|
171
|
+
code: "HOTLINE_HTTP_FAILED",
|
|
172
|
+
message: body?.error?.message || body?.message || `http adapter returned ${response.status}`,
|
|
173
|
+
retryable: false
|
|
174
|
+
},
|
|
175
|
+
schema_valid: true,
|
|
176
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return normalizeExecutionResult(body);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createConfiguredHotlineExecutor(hotline) {
|
|
184
|
+
const allowedTaskTypes = Array.isArray(hotline?.task_types) ? [...hotline.task_types] : null;
|
|
185
|
+
|
|
186
|
+
if (hotline?.adapter_type === "http") {
|
|
187
|
+
return {
|
|
188
|
+
name: `http-adapter:${hotline.hotline_id}`,
|
|
189
|
+
allowedTaskTypes,
|
|
190
|
+
async execute(context) {
|
|
191
|
+
return runHttpAdapter(hotline.adapter, context);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (hotline?.adapter_type === "function" && typeof hotline?.adapter?.fn === "function") {
|
|
197
|
+
return createFunctionExecutor(hotline.adapter.fn, {
|
|
198
|
+
name: `function-adapter:${hotline.hotline_id}`,
|
|
199
|
+
allowedTaskTypes
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
name: `process-adapter:${hotline?.hotline_id || "unknown"}`,
|
|
205
|
+
allowedTaskTypes,
|
|
206
|
+
async execute(context) {
|
|
207
|
+
return runProcessAdapter(hotline?.adapter || {}, context);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function createHotlineRouterExecutor(hotlines = [], fallback = createSimulatorExecutor()) {
|
|
213
|
+
const enabled = new Map(
|
|
214
|
+
(Array.isArray(hotlines) ? hotlines : [])
|
|
215
|
+
.filter((item) => item?.hotline_id)
|
|
216
|
+
.map((item) => [item.hotline_id, { definition: item, executor: createConfiguredHotlineExecutor(item) }])
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
name: "hotline-router-executor",
|
|
221
|
+
listHotlines() {
|
|
222
|
+
return Array.from(enabled.values()).map(({ definition }) => ({
|
|
223
|
+
hotline_id: definition.hotline_id,
|
|
224
|
+
display_name: definition.display_name || definition.hotline_id,
|
|
225
|
+
enabled: definition.enabled !== false,
|
|
226
|
+
adapter_type: definition.adapter_type || "process",
|
|
227
|
+
task_types: definition.task_types || [],
|
|
228
|
+
capabilities: definition.capabilities || [],
|
|
229
|
+
tags: definition.tags || []
|
|
230
|
+
}));
|
|
231
|
+
},
|
|
232
|
+
getAllowedTaskTypes(hotlineId) {
|
|
233
|
+
return enabled.get(hotlineId)?.executor.allowedTaskTypes || fallback?.allowedTaskTypes || null;
|
|
234
|
+
},
|
|
235
|
+
async execute(context) {
|
|
236
|
+
const selected = enabled.get(context.hotlineId);
|
|
237
|
+
if (!selected || selected.definition.enabled === false) {
|
|
238
|
+
if (fallback?.execute) {
|
|
239
|
+
return fallback.execute(context);
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
status: "error",
|
|
243
|
+
error: {
|
|
244
|
+
code: "HOTLINE_NOT_CONFIGURED",
|
|
245
|
+
message: `hotline '${context.hotlineId}' is not configured locally`,
|
|
246
|
+
retryable: false
|
|
247
|
+
},
|
|
248
|
+
schema_valid: true,
|
|
249
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return selected.executor.execute(context);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function createSimulatorExecutor() {
|
|
258
|
+
return createFunctionExecutor(
|
|
259
|
+
async ({ task }) => {
|
|
260
|
+
if (task.simulate === "timeout") {
|
|
261
|
+
return deferTask("timeout");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (task.simulate === "token_expired") {
|
|
265
|
+
return {
|
|
266
|
+
status: "error",
|
|
267
|
+
error: {
|
|
268
|
+
code: "AUTH_TOKEN_EXPIRED",
|
|
269
|
+
message: "Token expired during responder validation",
|
|
270
|
+
retryable: false
|
|
271
|
+
},
|
|
272
|
+
schema_valid: true,
|
|
273
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (task.simulate === "schema_invalid") {
|
|
278
|
+
return {
|
|
279
|
+
status: "ok",
|
|
280
|
+
output: { malformed_field: true },
|
|
281
|
+
schema_valid: false,
|
|
282
|
+
usage: { tokens_in: 12, tokens_out: 6 }
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (task.simulate === "reject") {
|
|
287
|
+
return {
|
|
288
|
+
status: "error",
|
|
289
|
+
error: {
|
|
290
|
+
code: "CONTRACT_REJECTED",
|
|
291
|
+
message: "Responder guardrail rejected this task",
|
|
292
|
+
retryable: false
|
|
293
|
+
},
|
|
294
|
+
schema_valid: true,
|
|
295
|
+
usage: { tokens_in: 0, tokens_out: 0 }
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
status: "ok",
|
|
301
|
+
output: {
|
|
302
|
+
summary: "Task completed",
|
|
303
|
+
task_id: task.task_id
|
|
304
|
+
},
|
|
305
|
+
schema_valid: true,
|
|
306
|
+
usage: { tokens_in: 42, tokens_out: 24 }
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
{ name: "simulator-executor" }
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function createExampleFunctionExecutor() {
|
|
314
|
+
return createFunctionExecutor(
|
|
315
|
+
async ({ taskInput, task }) => ({
|
|
316
|
+
status: "ok",
|
|
317
|
+
output: {
|
|
318
|
+
summary: `Handled ${task.task_type || "task"} for ${task.hotline_id}`,
|
|
319
|
+
received: taskInput ?? null
|
|
320
|
+
},
|
|
321
|
+
schema_valid: true,
|
|
322
|
+
usage: { tokens_in: 1, tokens_out: 1 }
|
|
323
|
+
}),
|
|
324
|
+
{ name: "example-function-executor" }
|
|
325
|
+
);
|
|
326
|
+
}
|