@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.
Files changed (55) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +6 -0
  3. package/node_modules/@delexec/caller-controller/README.md +3 -0
  4. package/node_modules/@delexec/caller-controller/README.zh-CN.md +6 -0
  5. package/node_modules/@delexec/caller-controller/package.json +53 -0
  6. package/node_modules/@delexec/caller-controller/src/server.js +127 -0
  7. package/node_modules/@delexec/caller-controller-core/README.md +3 -0
  8. package/node_modules/@delexec/caller-controller-core/README.zh-CN.md +6 -0
  9. package/node_modules/@delexec/caller-controller-core/package.json +26 -0
  10. package/node_modules/@delexec/caller-controller-core/src/index.js +1612 -0
  11. package/node_modules/@delexec/caller-skill-adapter/package.json +12 -0
  12. package/node_modules/@delexec/caller-skill-adapter/src/server.js +1042 -0
  13. package/node_modules/@delexec/caller-skill-mcp-adapter/README.md +65 -0
  14. package/node_modules/@delexec/caller-skill-mcp-adapter/package.json +16 -0
  15. package/node_modules/@delexec/caller-skill-mcp-adapter/src/server.js +527 -0
  16. package/node_modules/@delexec/responder-controller/README.md +3 -0
  17. package/node_modules/@delexec/responder-controller/README.zh-CN.md +6 -0
  18. package/node_modules/@delexec/responder-controller/package.json +53 -0
  19. package/node_modules/@delexec/responder-controller/src/server.js +254 -0
  20. package/node_modules/@delexec/responder-runtime-core/README.md +3 -0
  21. package/node_modules/@delexec/responder-runtime-core/README.zh-CN.md +6 -0
  22. package/node_modules/@delexec/responder-runtime-core/package.json +26 -0
  23. package/node_modules/@delexec/responder-runtime-core/src/executors.js +326 -0
  24. package/node_modules/@delexec/responder-runtime-core/src/index.js +1202 -0
  25. package/node_modules/@delexec/runtime-utils/README.md +3 -0
  26. package/node_modules/@delexec/runtime-utils/README.zh-CN.md +6 -0
  27. package/node_modules/@delexec/runtime-utils/package.json +23 -0
  28. package/node_modules/@delexec/runtime-utils/src/index.js +338 -0
  29. package/node_modules/@delexec/sqlite-store/README.md +3 -0
  30. package/node_modules/@delexec/sqlite-store/README.zh-CN.md +6 -0
  31. package/node_modules/@delexec/sqlite-store/package.json +26 -0
  32. package/node_modules/@delexec/sqlite-store/src/index.js +68 -0
  33. package/node_modules/@delexec/transport-email/README.md +3 -0
  34. package/node_modules/@delexec/transport-email/README.zh-CN.md +6 -0
  35. package/node_modules/@delexec/transport-email/package.json +23 -0
  36. package/node_modules/@delexec/transport-email/src/index.js +185 -0
  37. package/node_modules/@delexec/transport-emailengine/README.md +3 -0
  38. package/node_modules/@delexec/transport-emailengine/README.zh-CN.md +6 -0
  39. package/node_modules/@delexec/transport-emailengine/package.json +26 -0
  40. package/node_modules/@delexec/transport-emailengine/src/index.js +210 -0
  41. package/node_modules/@delexec/transport-gmail/README.md +3 -0
  42. package/node_modules/@delexec/transport-gmail/README.zh-CN.md +6 -0
  43. package/node_modules/@delexec/transport-gmail/package.json +26 -0
  44. package/node_modules/@delexec/transport-gmail/src/index.js +295 -0
  45. package/node_modules/@delexec/transport-relay-http/README.md +3 -0
  46. package/node_modules/@delexec/transport-relay-http/README.zh-CN.md +6 -0
  47. package/node_modules/@delexec/transport-relay-http/package.json +23 -0
  48. package/node_modules/@delexec/transport-relay-http/src/index.js +124 -0
  49. package/package.json +64 -0
  50. package/src/cli.js +1571 -0
  51. package/src/config.js +1180 -0
  52. package/src/example-hotline-worker.js +65 -0
  53. package/src/example-hotline.js +196 -0
  54. package/src/logging.js +56 -0
  55. 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,3 @@
1
+ # @delexec/responder-runtime-core
2
+
3
+ Core responder runtime logic for delegated execution clients.
@@ -0,0 +1,6 @@
1
+ # @delexec/responder-runtime-core
2
+
3
+ > 英文版:README.md
4
+ > 说明:中文文档为准。
5
+
6
+ 委托执行客户端的 responder 运行时核心逻辑。
@@ -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
+ }