@faapi/faapi 6.5.0 → 6.7.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as AppRegistries, R as RouteManifest, F as FaapiContext, C as CorsOptions, T as TaskClient, H as HelmetOptions, L as LoggerOptions, a as FaapiMiddleware, I as InjectorMap, b as TaskFailedHandler, c as TaskQueueDeps, d as TaskQueue, e as TaskDriver, f as TaskRegistry, g as TaskManifest, h as ToolMetadata, i as AgentCore, j as AgentMetadata, W as WsRouteManifest } from './routeTypes-q9OXB--o.js';
2
- export { k as AgentHandleFactory, l as AgentHandleStore, m as AgentPathMeta, n as AgentRegistry, o as AgentToolDescriptor, p as FaapiContextConfig, q as FaapiTaskMeta, r as FailOptions, s as Injector, t as RouteInfo, u as RouteInputSchema, v as RouteOutputSchema, w as RouteParamSchema, S as SkillRegistry, x as SseEvent, y as SseWriter, z as TaskContext, B as TaskDriverJob, D as TaskDriverProcess, E as TaskDriverRecord, G as TaskFailedInfo, J as TaskJob, K as TaskJobStatus, M as TaskMetadata, N as TaskModule, O as TaskRegistriesSnapshot, P as TaskRegistriesView, Q as ToolCore, U as ToolPathMeta, V as ToolRegistry, X as cors, Y as createAppRegistries, Z as createTaskRegistriesView, _ as createTaskRegistry, $ as helmet, a0 as logger } from './routeTypes-q9OXB--o.js';
1
+ import { A as AppRegistries, R as RouteManifest, F as FaapiContext, C as CorsOptions, T as TaskClient, H as HelmetOptions, L as LoggerOptions, a as FaapiMiddleware, I as InjectorMap, b as TaskFailedHandler, c as TaskQueueDeps, d as TaskQueue, e as TaskDriver, f as TaskRegistry, g as TaskManifest, h as ToolMetadata, i as AgentCore, j as AgentMetadata, W as WsRouteManifest } from './routeTypes-CsL7xtqu.js';
2
+ export { k as AgentHandleFactory, l as AgentHandleStore, m as AgentPathMeta, n as AgentRegistry, o as AgentToolDescriptor, p as FaapiContextConfig, q as FaapiTaskMeta, r as FailOptions, s as Injector, t as RouteInfo, u as RouteInputSchema, v as RouteOutputSchema, w as RouteParamSchema, S as SkillRegistry, x as SseEvent, y as SseWriter, z as TaskContext, B as TaskDriverJob, D as TaskDriverProcess, E as TaskDriverRecord, G as TaskFailedInfo, J as TaskJob, K as TaskJobStatus, M as TaskMetadata, N as TaskModule, O as TaskRegistriesSnapshot, P as TaskRegistriesView, Q as ToolCore, U as ToolPathMeta, V as ToolRegistry, X as cors, Y as createAppRegistries, Z as createTaskRegistriesView, _ as createTaskRegistry, $ as helmet, a0 as logger } from './routeTypes-CsL7xtqu.js';
3
3
  import * as node_http from 'node:http';
4
4
  import { Server, IncomingMessage, ServerResponse } from 'node:http';
5
5
  import { Socket } from 'node:net';
package/dist/index.js CHANGED
@@ -2053,6 +2053,19 @@ function buildRegistriesView(snapshot) {
2053
2053
  };
2054
2054
  }
2055
2055
  `;
2056
+ var SERIALIZE_ERROR_SOURCE = `
2057
+ function serializeError(err) {
2058
+ if (err instanceof Error) {
2059
+ var props = {};
2060
+ var keys = Object.keys(err);
2061
+ for (var i = 0; i < keys.length; i++) {
2062
+ props[keys[i]] = err[keys[i]];
2063
+ }
2064
+ return { name: err.name, message: err.message, stack: err.stack, props: props };
2065
+ }
2066
+ return { name: 'Error', message: String(err), stack: undefined, props: {} };
2067
+ }
2068
+ `;
2056
2069
  function buildWrapperSource(moduleUrl) {
2057
2070
  return `
2058
2071
  import { parentPort } from 'node:worker_threads';
@@ -2060,9 +2073,14 @@ import * as mod from '${moduleUrl}';
2060
2073
 
2061
2074
  ${BUILD_VIEW_SOURCE}
2062
2075
 
2076
+ ${SERIALIZE_ERROR_SOURCE}
2077
+
2063
2078
  const run = mod.run;
2064
2079
  if (typeof run !== 'function') {
2065
- parentPort.postMessage({ type: 'error', message: 'Task module has no run export' });
2080
+ parentPort.postMessage({
2081
+ type: 'error',
2082
+ error: { name: 'Error', message: 'Task module has no run export', stack: undefined, props: {} },
2083
+ });
2066
2084
  } else {
2067
2085
  let controller = null;
2068
2086
  parentPort.on('message', async (msg) => {
@@ -2075,21 +2093,44 @@ if (typeof run !== 'function') {
2075
2093
  const { payload, taskCtx } = msg;
2076
2094
  try {
2077
2095
  const registries = buildRegistriesView(msg.registries);
2078
- const result = await run(payload, { ...taskCtx, signal: controller.signal, registries });
2096
+ const progress = (value) => parentPort.postMessage({ type: 'progress', value });
2097
+ const result = await run(payload, { ...taskCtx, signal: controller.signal, registries, progress });
2079
2098
  parentPort.postMessage({ type: 'done', result });
2080
2099
  } catch (err) {
2081
- parentPort.postMessage({
2082
- type: 'error',
2083
- message: err instanceof Error ? err.message : String(err),
2084
- });
2100
+ const serialized = serializeError(err);
2101
+ try {
2102
+ parentPort.postMessage({ type: 'error', error: serialized });
2103
+ } catch (_) {
2104
+ // props \u542B\u4E0D\u53EF\u514B\u9686\u503C\uFF08\u51FD\u6570\u7B49\uFF09\u65F6\u4E22\u5F03 props\uFF0C\u4FDD\u5E95 name/message/stack
2105
+ parentPort.postMessage({
2106
+ type: 'error',
2107
+ error: { name: serialized.name, message: serialized.message, stack: serialized.stack, props: {} },
2108
+ });
2109
+ }
2085
2110
  }
2086
2111
  });
2087
2112
  }
2088
2113
  `;
2089
2114
  }
2115
+ function reviveError(payload) {
2116
+ const err = new Error(payload?.message ?? "task worker error");
2117
+ if (payload?.name !== void 0) err.name = payload.name;
2118
+ if (payload?.stack !== void 0) err.stack = payload.stack;
2119
+ if (payload?.props) {
2120
+ for (const [key, value] of Object.entries(payload.props)) {
2121
+ err[key] = value;
2122
+ }
2123
+ }
2124
+ return err;
2125
+ }
2090
2126
  async function runTaskInWorker(options) {
2091
2127
  const { taskModulePath, payload, taskCtx, timeoutMs, externalSignal } = options;
2092
- const killGraceMs = options.killGraceMs ?? KILL_GRACE_MS;
2128
+ const killGraceMs = options.graceMs ?? KILL_GRACE_MS;
2129
+ if (externalSignal?.aborted) {
2130
+ throw new TaskCancelledError(
2131
+ `Task "${taskCtx.job.name}" cancelled before start (driver stop signal already aborted)`
2132
+ );
2133
+ }
2093
2134
  const moduleUrl = pathToFileURL(taskModulePath).href;
2094
2135
  const wrapperUrl = new URL(
2095
2136
  `data:text/javascript,${encodeURIComponent(buildWrapperSource(moduleUrl))}`
@@ -2137,27 +2178,37 @@ async function runTaskInWorker(options) {
2137
2178
  () => startCancel(`timed out after ${timeoutMs}ms`),
2138
2179
  timeoutMs
2139
2180
  );
2140
- worker.on("message", (msg) => {
2141
- if (phase === "grace") {
2142
- if (msg?.type === "done") {
2143
- finish(
2144
- () => reject(
2145
- new TaskCancelledError(
2146
- `Task "${taskCtx.job.name}" ${cancelReason} (task completed after the timeout)`
2181
+ worker.on(
2182
+ "message",
2183
+ (msg) => {
2184
+ if (phase === "grace") {
2185
+ if (msg?.type === "done") {
2186
+ finish(
2187
+ () => reject(
2188
+ new TaskCancelledError(
2189
+ `Task "${taskCtx.job.name}" ${cancelReason} (task completed after the timeout)`
2190
+ )
2147
2191
  )
2148
- )
2149
- );
2192
+ );
2193
+ } else if (msg?.type === "error") {
2194
+ finish(() => reject(new TaskCancelledError(msg.error?.message ?? cancelReason)));
2195
+ }
2196
+ return;
2197
+ }
2198
+ if (phase !== "running") return;
2199
+ if (msg?.type === "done") {
2200
+ finish(() => resolve(msg.result));
2150
2201
  } else if (msg?.type === "error") {
2151
- finish(() => reject(new TaskCancelledError(msg.message ?? cancelReason)));
2202
+ finish(() => reject(reviveError(msg.error)));
2203
+ } else if (msg?.type === "progress") {
2204
+ options.onProgress?.(msg.value);
2152
2205
  }
2153
- return;
2154
- }
2155
- if (phase !== "running") return;
2156
- if (msg?.type === "done") {
2157
- finish(() => resolve(msg.result));
2158
- } else if (msg?.type === "error") {
2159
- finish(() => reject(new Error(msg.message ?? "task worker error")));
2160
2206
  }
2207
+ );
2208
+ worker.on("messageerror", () => {
2209
+ finish(
2210
+ () => reject(new Error(`Task "${taskCtx.job.name}" worker message could not be deserialized`))
2211
+ );
2161
2212
  });
2162
2213
  worker.on("error", (err) => {
2163
2214
  finish(() => reject(err));
@@ -2187,6 +2238,8 @@ async function runTaskInWorker(options) {
2187
2238
  }
2188
2239
 
2189
2240
  // src/task/taskQueue.ts
2241
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
2242
+ var MAX_FINISHED_RECORDS = 1e3;
2190
2243
  function createTaskQueue(deps) {
2191
2244
  const { registry, rootDir, driver } = deps;
2192
2245
  const registriesView = deps.registries ?? createEmptyTaskRegistriesView();
@@ -2195,6 +2248,25 @@ function createTaskQueue(deps) {
2195
2248
  const schemaCache = /* @__PURE__ */ new Map();
2196
2249
  let started = false;
2197
2250
  let stopped = false;
2251
+ let finishedCount = 0;
2252
+ function transitionStatus(record, status) {
2253
+ const wasTerminal = TERMINAL_STATUSES.has(record.status);
2254
+ const nowTerminal = TERMINAL_STATUSES.has(status);
2255
+ if (!wasTerminal && nowTerminal) finishedCount++;
2256
+ else if (wasTerminal && !nowTerminal) finishedCount--;
2257
+ record.status = status;
2258
+ }
2259
+ function evictFinishedRecords() {
2260
+ let toEvict = finishedCount - MAX_FINISHED_RECORDS;
2261
+ for (const [id, job] of records) {
2262
+ if (toEvict <= 0) break;
2263
+ if (TERMINAL_STATUSES.has(job.status)) {
2264
+ records.delete(id);
2265
+ finishedCount--;
2266
+ toEvict--;
2267
+ }
2268
+ }
2269
+ }
2198
2270
  const loadTaskModule = deps.loadTaskModule ?? (async (filePath) => await import(filePath));
2199
2271
  const loadPayloadSchema = deps.loadPayloadSchema ?? (async (filePath) => {
2200
2272
  const zodPath = path3.join(path3.dirname(filePath), "zod.js");
@@ -2253,7 +2325,8 @@ function createTaskQueue(deps) {
2253
2325
  createdAt: Date.now()
2254
2326
  };
2255
2327
  record.attempts = job.attempt;
2256
- record.status = "running";
2328
+ transitionStatus(record, "running");
2329
+ delete record.progress;
2257
2330
  record.error = void 0;
2258
2331
  records.set(job.id, record);
2259
2332
  try {
@@ -2268,7 +2341,11 @@ function createTaskQueue(deps) {
2268
2341
  },
2269
2342
  registries: snapshotRegistries(),
2270
2343
  timeoutMs: meta.timeoutMs,
2271
- externalSignal: job.signal
2344
+ graceMs: meta.graceMs,
2345
+ externalSignal: job.signal,
2346
+ onProgress: (value) => {
2347
+ if (record.status === "running") record.progress = value;
2348
+ }
2272
2349
  });
2273
2350
  } else {
2274
2351
  let mod = moduleCache2.get(job.name);
@@ -2283,17 +2360,22 @@ function createTaskQueue(deps) {
2283
2360
  signal: job.signal,
2284
2361
  config: deps.config,
2285
2362
  job: { id: job.id, name: job.name, attempt: job.attempt },
2286
- registries: registriesView
2363
+ registries: registriesView,
2364
+ progress: (value) => {
2365
+ if (record.status === "running") record.progress = value;
2366
+ }
2287
2367
  };
2288
2368
  result = await mod.run(job.payload, taskCtx);
2289
2369
  }
2290
- record.status = "done";
2370
+ transitionStatus(record, "done");
2291
2371
  record.result = result;
2372
+ evictFinishedRecords();
2292
2373
  return result;
2293
2374
  } catch (err) {
2294
2375
  const cancelled = err instanceof TaskCancelledError || job.signal.aborted;
2295
- record.status = cancelled ? "cancelled" : "failed";
2376
+ transitionStatus(record, cancelled ? "cancelled" : "failed");
2296
2377
  record.error = err instanceof Error ? err.message : String(err);
2378
+ evictFinishedRecords();
2297
2379
  if (deps.onFailed) {
2298
2380
  void Promise.resolve().then(
2299
2381
  () => deps.onFailed({
@@ -2388,7 +2470,8 @@ function createTaskQueue(deps) {
2388
2470
  }
2389
2471
  await driver.cancel(name, id);
2390
2472
  const record = records.get(id);
2391
- if (record) record.status = "cancelled";
2473
+ if (record) transitionStatus(record, "cancelled");
2474
+ evictFinishedRecords();
2392
2475
  },
2393
2476
  async retry(name, id) {
2394
2477
  if (!driver.retry) {
@@ -2398,7 +2481,7 @@ function createTaskQueue(deps) {
2398
2481
  }
2399
2482
  await driver.retry(name, id);
2400
2483
  const record = records.get(id);
2401
- if (record) record.status = "pending";
2484
+ if (record) transitionStatus(record, "pending");
2402
2485
  },
2403
2486
  async start() {
2404
2487
  if (stopped) {
@@ -2506,9 +2589,15 @@ import fs2 from "fs";
2506
2589
  var TASK_PATTERNS = ["src/tasks/**/task.ts"];
2507
2590
  var TASK_FILENAME = "task.ts";
2508
2591
  var CRON_RE = /(?:^|\n)\s*cron:\s*['"`]([^'"`\n]+)['"`]/;
2509
- var CONCURRENCY_RE = /(?:^|\n)\s*concurrency:\s*(\d+)/;
2510
- var RETRIES_RE = /(?:^|\n)\s*retries:\s*(\d+)/;
2511
- var TIMEOUT_MS_RE = /(?:^|\n)\s*timeoutMs:\s*(\d+)/;
2592
+ var NUM_LITERAL = "(\\d+(?:_\\d+)*)";
2593
+ var CONCURRENCY_RE = new RegExp(`(?:^|\\n)\\s*concurrency:\\s*${NUM_LITERAL}`);
2594
+ var RETRIES_RE = new RegExp(`(?:^|\\n)\\s*retries:\\s*${NUM_LITERAL}`);
2595
+ var TIMEOUT_MS_RE = new RegExp(`(?:^|\\n)\\s*timeoutMs:\\s*${NUM_LITERAL}`);
2596
+ var GRACE_MS_RE = new RegExp(`(?:^|\\n)\\s*graceMs:\\s*${NUM_LITERAL}`);
2597
+ function parseNumericLiteral(raw) {
2598
+ return Number(raw.replace(/_/g, ""));
2599
+ }
2600
+ var MIN_ISOLATED_TIMEOUT_MS = 6e4;
2512
2601
  function filePathToTaskName(filePath) {
2513
2602
  const normalized = filePath.replace(/\\/g, "/");
2514
2603
  const match = normalized.match(/(?:^|\/)tasks\/(.+)\/task\.ts$/);
@@ -2546,13 +2635,23 @@ async function scanTasks(rootDir, patterns) {
2546
2635
  const concurrency = CONCURRENCY_RE.exec(source)?.[1];
2547
2636
  const retries = RETRIES_RE.exec(source)?.[1];
2548
2637
  const timeoutMs = TIMEOUT_MS_RE.exec(source)?.[1];
2638
+ const graceMs = GRACE_MS_RE.exec(source)?.[1];
2549
2639
  if (cron !== void 0) manifest.cron = cron;
2550
- if (concurrency !== void 0) manifest.concurrency = Number(concurrency);
2551
- if (retries !== void 0) manifest.retries = Number(retries);
2552
- if (timeoutMs !== void 0) manifest.timeoutMs = Number(timeoutMs);
2640
+ if (concurrency !== void 0) manifest.concurrency = parseNumericLiteral(concurrency);
2641
+ if (retries !== void 0) manifest.retries = parseNumericLiteral(retries);
2642
+ if (timeoutMs !== void 0) {
2643
+ const value = parseNumericLiteral(timeoutMs);
2644
+ if (value < MIN_ISOLATED_TIMEOUT_MS) {
2645
+ throw new Error(
2646
+ `[faapi] Task "${name}" timeoutMs ${value} is below the minimum ${MIN_ISOLATED_TIMEOUT_MS}ms (1 minute). Declaring a timeout means this is a long-running task that needs real cancellation \u2014 tasks finishing within a minute do not need one: remove timeoutMs to run in-process (implement your own deadline with Promise.race if needed), or raise it.`
2647
+ );
2648
+ }
2649
+ manifest.timeoutMs = value;
2650
+ }
2651
+ if (graceMs !== void 0) manifest.graceMs = parseNumericLiteral(graceMs);
2553
2652
  tasks.push(manifest);
2554
2653
  }
2555
- return tasks;
2654
+ return tasks.sort((a, b) => a.name.localeCompare(b.name));
2556
2655
  }
2557
2656
 
2558
2657
  // src/injection/agentRegistry.ts
@@ -6116,7 +6215,8 @@ function serializeTasks(tasks, dist = "dist") {
6116
6215
  ...t.cron !== void 0 ? { cron: t.cron } : {},
6117
6216
  ...t.concurrency !== void 0 ? { concurrency: t.concurrency } : {},
6118
6217
  ...t.retries !== void 0 ? { retries: t.retries } : {},
6119
- ...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {}
6218
+ ...t.timeoutMs !== void 0 ? { timeoutMs: t.timeoutMs } : {},
6219
+ ...t.graceMs !== void 0 ? { graceMs: t.graceMs } : {}
6120
6220
  }));
6121
6221
  }
6122
6222
  async function writeTasksModule(manifest, outputPath) {
@@ -6132,7 +6232,8 @@ function hydrateTasks(manifest) {
6132
6232
  cron: t.cron ?? void 0,
6133
6233
  concurrency: t.concurrency ?? void 0,
6134
6234
  retries: t.retries ?? void 0,
6135
- timeoutMs: t.timeoutMs ?? void 0
6235
+ timeoutMs: t.timeoutMs ?? void 0,
6236
+ graceMs: t.graceMs ?? void 0
6136
6237
  }));
6137
6238
  }
6138
6239
  function collectTaskSchemaSources(tasks, rootDir) {