refactor(workflow): move IPC, worker, manager from daemon to @uncaged/workflow
- Move workflow IPC types (StartThread, ResumeThread, etc.) to workflow/ipc.ts - Move workflow-worker.ts, workflow-manager.ts, workflow-manager-support.ts - Move worker-runtime.ts and worker-signals.ts (shared infrastructure) - Daemon now imports workflow runtime from @uncaged/workflow - Export WORKFLOW_WORKER_PATH for daemon to spawn workers Phase 3+4 of #320, Testing: #322
This commit is contained in:
@@ -20,7 +20,7 @@ Always use static top-level `import` statements.
|
|||||||
## Exceptions (must include a comment explaining why)
|
## Exceptions (must include a comment explaining why)
|
||||||
|
|
||||||
1. **`sense-runtime.ts`** — loads user-authored sense modules whose paths are only known at runtime
|
1. **`sense-runtime.ts`** — loads user-authored sense modules whose paths are only known at runtime
|
||||||
2. **`workflow-worker.ts`** — loads user-authored workflow modules whose paths are only known at runtime
|
2. **`packages/workflow/src/worker.ts`** — loads user-authored workflow modules whose paths are only known at runtime
|
||||||
|
|
||||||
When suppressing, add a comment directly above:
|
When suppressing, add a comment directly above:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"build": "pnpm -r run build",
|
"build": "pnpm --filter @uncaged/workflow run build:public-types && pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-store run build && pnpm --filter @uncaged/workflow run build && pnpm -r --filter '!@uncaged/nerve-core' --filter '!@uncaged/nerve-store' --filter '!@uncaged/workflow' run build",
|
||||||
"test": "pnpm -r test",
|
"test": "pnpm -r test",
|
||||||
"check": "biome check .",
|
"check": "biome check .",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
||||||
"build": "rslib build",
|
"build": "rslib build",
|
||||||
"pretest": "pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-daemon run build",
|
"pretest": "pnpm --filter @uncaged/workflow run build:public-types && pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-store run build && pnpm --filter @uncaged/workflow run build && pnpm --filter @uncaged/nerve-daemon run build",
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"@rslib/core": "^0.21.3",
|
"@rslib/core": "^0.21.3",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@uncaged/nerve-daemon": "workspace:*",
|
"@uncaged/nerve-daemon": "workspace:*",
|
||||||
|
"@uncaged/workflow": "workspace:*",
|
||||||
"vitest": "^4.1.5"
|
"vitest": "^4.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ import { workflowCommand } from "../commands/workflow.js";
|
|||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const nerveDaemonRoot = dirname(require.resolve("@uncaged/nerve-daemon/package.json"));
|
const nerveDaemonRoot = dirname(require.resolve("@uncaged/nerve-daemon/package.json"));
|
||||||
|
const nerveWorkflowRoot = dirname(require.resolve("@uncaged/workflow/package.json"));
|
||||||
const senseWorkerScript = join(nerveDaemonRoot, "dist", "sense-worker.js");
|
const senseWorkerScript = join(nerveDaemonRoot, "dist", "sense-worker.js");
|
||||||
const workflowWorkerScript = join(nerveDaemonRoot, "dist", "workflow-worker.js");
|
const workflowWorkerScript = join(nerveWorkflowRoot, "dist", "worker.js");
|
||||||
|
|
||||||
const nerveYamlTemplate = `senses:
|
const nerveYamlTemplate = `senses:
|
||||||
counter:
|
counter:
|
||||||
@@ -274,7 +275,7 @@ export async function startTestDaemon(
|
|||||||
}
|
}
|
||||||
if (!existsSync(workflowWorkerScript)) {
|
if (!existsSync(workflowWorkerScript)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Missing "${workflowWorkerScript}". Run \`pnpm --filter @uncaged/nerve-daemon build\`.`,
|
`Missing "${workflowWorkerScript}". Run \`pnpm --filter @uncaged/workflow build\`.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Extract layer types — parses agent raw string output into typed meta (RFC-003).
|
||||||
|
*/
|
||||||
|
/** Structured meta validation descriptor for `ExtractFn`; concrete validators are provider-defined. */
|
||||||
|
export type Schema<T> = {
|
||||||
|
readonly witness: T | null;
|
||||||
|
};
|
||||||
|
export type ExtractFn<T> = (raw: string, schema: Schema<T>) => Promise<T>;
|
||||||
|
export declare class ExtractError extends Error {
|
||||||
|
readonly raw: string;
|
||||||
|
readonly causeError: Error | null;
|
||||||
|
constructor(message: string, detail: {
|
||||||
|
raw: string;
|
||||||
|
causeError: Error | null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=agent.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["agent.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,uGAAuG;AACvG,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI;IACtB,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;AAE1E,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,KAAK,GAAG,IAAI,CAAC;gBAEtB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,KAAK,GAAG,IAAI,CAAA;KAAE;CAO/E"}
|
||||||
Vendored
+63
@@ -0,0 +1,63 @@
|
|||||||
|
import { type DropOverflowConfig, type QueueOverflowConfig, type WorkflowConfig } from "@uncaged/workflow";
|
||||||
|
import { type Result } from "./util.js";
|
||||||
|
export type { DropOverflowConfig, QueueOverflowConfig, WorkflowConfig };
|
||||||
|
export type SenseConfig = {
|
||||||
|
group: string;
|
||||||
|
throttle: number | null;
|
||||||
|
timeout: number | null;
|
||||||
|
gracePeriod: number | null;
|
||||||
|
/** Polling interval (ms). When set, the sense is triggered periodically. */
|
||||||
|
interval: number | null;
|
||||||
|
/** Other sense names whose successful computes schedule this sense (kernel reverse-index). */
|
||||||
|
on: string[];
|
||||||
|
};
|
||||||
|
/** Optional HTTP control plane. When `port` is null, the HTTP server is not started. */
|
||||||
|
export type NerveApiConfig = {
|
||||||
|
port: number | null;
|
||||||
|
/** When set, HTTP API requires `Authorization: Bearer <token>`. */
|
||||||
|
token: string | null;
|
||||||
|
/** Bind address (e.g. `127.0.0.1`, `0.0.0.0`). Meaningful when `port` is set. */
|
||||||
|
host: string;
|
||||||
|
};
|
||||||
|
/** Adapter factory input (model, timeout); used by adapter packages (RFC-003). */
|
||||||
|
export type AgentConfig = {
|
||||||
|
/** Adapter id (e.g. `cursor`, `hermes`, `echo`) — informational for factories that branch on type. */
|
||||||
|
type: string;
|
||||||
|
/** Model id or `"auto"` for adapter defaults. */
|
||||||
|
model: string;
|
||||||
|
/** Wall-clock cap in milliseconds, or `null` for adapter-specific default. */
|
||||||
|
timeout: number | null;
|
||||||
|
};
|
||||||
|
/** Global extract provider for typed meta from agent raw output (RFC-003). */
|
||||||
|
export type ExtractConfig = {
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Optional shell side effect after a successful sense `compute()`.
|
||||||
|
* Executed in the sense worker (`spawn` with `shell: true`, cwd = nerve root).
|
||||||
|
* Workflows are started only via CLI / daemon IPC, not from sense compute results.
|
||||||
|
*/
|
||||||
|
export type SenseTrigger = {
|
||||||
|
command: string;
|
||||||
|
};
|
||||||
|
export type NerveConfig = {
|
||||||
|
/** Engine-wide default max moderator rounds (e.g. CLI workflow trigger when omitted). */
|
||||||
|
maxRounds: number;
|
||||||
|
senses: Record<string, SenseConfig>;
|
||||||
|
workflows: Record<string, WorkflowConfig>;
|
||||||
|
api: NerveApiConfig;
|
||||||
|
/** Global extract defaults; `null` when the section is omitted. */
|
||||||
|
extract: ExtractConfig | null;
|
||||||
|
};
|
||||||
|
export type KnowledgeConfig = {
|
||||||
|
include: ReadonlyArray<string>;
|
||||||
|
exclude: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
export declare function parseNerveConfig(raw: string): Result<NerveConfig>;
|
||||||
|
/**
|
||||||
|
* Parse `knowledge.yaml` at the repo root (RFC-003 Knowledge Layer).
|
||||||
|
* `include` / `exclude` entries are glob patterns resolved against the repo root.
|
||||||
|
*/
|
||||||
|
export declare function parseKnowledgeYaml(raw: string): Result<KnowledgeConfig>;
|
||||||
|
//# sourceMappingURL=config.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,MAAM,EAAmD,MAAM,WAAW,CAAC;AAEzF,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,cAAc,EAAE,CAAC;AAExE,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,8FAA8F;IAC9F,EAAE,EAAE,MAAM,EAAE,CAAC;CACd,CAAC;AAEF,wFAAwF;AACxF,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,mEAAmE;IACnE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,kFAAkF;AAClF,MAAM,MAAM,WAAW,GAAG;IACxB,sGAAsG;IACtG,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,GAAG,EAAE,cAAc,CAAC;IACpB,mEAAmE;IACnE,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChC,CAAC;AAuQF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,CAwDjE;AAoBD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,eAAe,CAAC,CA8BvE"}
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type DropOverflowConfig,
|
type DropOverflowConfig,
|
||||||
type QueueOverflowConfig,
|
type QueueOverflowConfig,
|
||||||
type WorkflowConfig,
|
type WorkflowConfig,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow/public-types";
|
||||||
import { type Result, err, isPlainRecord, ok, parseDurationStringToMs } from "./util.js";
|
import { type Result, err, isPlainRecord, ok, parseDurationStringToMs } from "./util.js";
|
||||||
|
|
||||||
export type { DropOverflowConfig, QueueOverflowConfig, WorkflowConfig };
|
export type { DropOverflowConfig, QueueOverflowConfig, WorkflowConfig };
|
||||||
|
|||||||
Vendored
+120
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Daemon Unix-socket IPC protocol (CLI → daemon).
|
||||||
|
* Newline-delimited JSON: one request object per line from the client,
|
||||||
|
* one response object per line from the daemon.
|
||||||
|
*/
|
||||||
|
import type { SenseInfo } from "./sense.js";
|
||||||
|
/** Runtime status of a registered workflow (for listing / observability). */
|
||||||
|
export type WorkflowStatus = {
|
||||||
|
name: string;
|
||||||
|
activeThreads: number;
|
||||||
|
/** Run IDs currently executing (same identifiers accepted by kill-workflow). */
|
||||||
|
activeRunIds: string[];
|
||||||
|
queuedThreads: number;
|
||||||
|
config: {
|
||||||
|
concurrency: number;
|
||||||
|
overflow: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** Public health payload for HTTP / IPC. */
|
||||||
|
export type HealthInfo = {
|
||||||
|
ok: boolean;
|
||||||
|
version: string;
|
||||||
|
uptime: number;
|
||||||
|
startedAt: string;
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
/** Client → daemon: start a workflow run. */
|
||||||
|
export type DaemonIpcTriggerWorkflowRequest = {
|
||||||
|
type: "trigger-workflow";
|
||||||
|
workflow: string;
|
||||||
|
prompt: string;
|
||||||
|
maxRounds: number;
|
||||||
|
dryRun: boolean;
|
||||||
|
};
|
||||||
|
/** Client → daemon: run a sense compute on demand. */
|
||||||
|
export type DaemonIpcTriggerSenseRequest = {
|
||||||
|
type: "trigger-sense";
|
||||||
|
sense: string;
|
||||||
|
};
|
||||||
|
/** Client → daemon: list registered senses. */
|
||||||
|
export type DaemonIpcListSensesRequest = {
|
||||||
|
type: "list-senses";
|
||||||
|
};
|
||||||
|
/** Client → daemon: kill a running or queued workflow thread by runId. */
|
||||||
|
export type DaemonIpcKillWorkflowRequest = {
|
||||||
|
type: "kill-workflow";
|
||||||
|
runId: string;
|
||||||
|
};
|
||||||
|
/** Client → daemon: list registered workflows and queue/active counts. */
|
||||||
|
export type DaemonIpcListWorkflowsRequest = {
|
||||||
|
type: "list-workflows";
|
||||||
|
};
|
||||||
|
/** Client → daemon: public health snapshot. */
|
||||||
|
export type DaemonIpcHealthRequest = {
|
||||||
|
type: "health";
|
||||||
|
};
|
||||||
|
/** Union of all JSON requests the daemon IPC server accepts. */
|
||||||
|
export type DaemonIpcRequest = DaemonIpcTriggerWorkflowRequest | DaemonIpcTriggerSenseRequest | DaemonIpcListSensesRequest | DaemonIpcKillWorkflowRequest | DaemonIpcListWorkflowsRequest | DaemonIpcHealthRequest;
|
||||||
|
/** Successful trigger / trigger-sense reply (no body). */
|
||||||
|
export type DaemonIpcTriggerOkResponse = {
|
||||||
|
ok: true;
|
||||||
|
};
|
||||||
|
export type DaemonIpcErrorResponse = {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
/** Replies for trigger-workflow and trigger-sense. */
|
||||||
|
export type DaemonIpcTriggerResponse = DaemonIpcTriggerOkResponse | DaemonIpcErrorResponse;
|
||||||
|
/** Reply for list-senses. */
|
||||||
|
export type DaemonIpcListSensesResponse = {
|
||||||
|
ok: true;
|
||||||
|
senses: SenseInfo[];
|
||||||
|
} | DaemonIpcErrorResponse;
|
||||||
|
/** Reply for list-workflows. */
|
||||||
|
export type DaemonIpcListWorkflowsResponse = {
|
||||||
|
ok: true;
|
||||||
|
workflows: WorkflowStatus[];
|
||||||
|
} | DaemonIpcErrorResponse;
|
||||||
|
/** Reply for health. */
|
||||||
|
export type DaemonIpcHealthResponse = {
|
||||||
|
ok: true;
|
||||||
|
health: HealthInfo;
|
||||||
|
} | DaemonIpcErrorResponse;
|
||||||
|
/** Any JSON response the daemon may write on the IPC socket. */
|
||||||
|
export type DaemonIpcResponse = DaemonIpcTriggerOkResponse | DaemonIpcErrorResponse | DaemonIpcListSensesResponse | DaemonIpcListWorkflowsResponse | DaemonIpcHealthResponse;
|
||||||
|
export type DaemonTransportTriggerResult = {
|
||||||
|
ok: true;
|
||||||
|
} | {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
export type DaemonTransportWorkflowLaunch = {
|
||||||
|
prompt: string;
|
||||||
|
maxRounds: number;
|
||||||
|
dryRun: boolean;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Abstraction over daemon control plane (Unix socket IPC today, HTTP in Phase 2).
|
||||||
|
* Implementations live in CLI / tools; the daemon kernel uses shared handler logic.
|
||||||
|
*/
|
||||||
|
export type DaemonTransport = {
|
||||||
|
health(): Promise<HealthInfo>;
|
||||||
|
listSenses(): Promise<SenseInfo[]>;
|
||||||
|
listWorkflows(): Promise<WorkflowStatus[]>;
|
||||||
|
triggerSense(name: string): Promise<DaemonTransportTriggerResult>;
|
||||||
|
/** When `launch` is null, implementations use engine defaults (empty prompt, default max rounds, dryRun false). */
|
||||||
|
triggerWorkflow(name: string, launch: DaemonTransportWorkflowLaunch | null): Promise<DaemonTransportTriggerResult>;
|
||||||
|
/** Kill a running or queued workflow thread by `runId` (same field as IPC `kill-workflow`). */
|
||||||
|
killWorkflow(runId: string): Promise<DaemonTransportTriggerResult>;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Parse a single line of JSON into a {@link DaemonIpcRequest}, or null if invalid.
|
||||||
|
* Kept in core with the request types so CLI and daemon stay aligned at compile time.
|
||||||
|
*/
|
||||||
|
export declare function parseDaemonIpcRequest(line: string): DaemonIpcRequest | null;
|
||||||
|
/** Type guard for JSON {@link SenseInfo} payloads from daemon HTTP/IPC. */
|
||||||
|
export declare function isSenseInfo(value: unknown): value is SenseInfo;
|
||||||
|
/** Type guard for JSON {@link WorkflowStatus} payloads from daemon HTTP/IPC. */
|
||||||
|
export declare function isWorkflowStatus(value: unknown): value is WorkflowStatus;
|
||||||
|
//# sourceMappingURL=daemon.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["daemon.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
export type { SenseConfig, DropOverflowConfig, QueueOverflowConfig, WorkflowConfig, NerveApiConfig, AgentConfig, ExtractConfig, NerveConfig, SenseTrigger, } from "./config.js";
|
||||||
|
export type { SenseInfo } from "./sense.js";
|
||||||
|
export type { SenseComputeFn, SenseModule } from "./sense.js";
|
||||||
|
export { senseTriggerLabels } from "./sense.js";
|
||||||
|
export type { WorkflowMessage, RoleResult, Role, RoleMeta, StartStep, ThreadContext, WorkflowContext, AgentFn, RoleStep, ModeratorContext, Moderator, WorkflowDefinition, } from "@uncaged/workflow";
|
||||||
|
export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "@uncaged/workflow";
|
||||||
|
export type { Schema, ExtractFn } from "./agent.js";
|
||||||
|
export { ExtractError } from "./agent.js";
|
||||||
|
export type { Result } from "./util.js";
|
||||||
|
export { ok, err } from "./util.js";
|
||||||
|
export { nerveCommandEnv, spawnSafe, type SpawnEnv, type SpawnError, type SpawnResult, type SpawnSafeOptions, } from "./util.js";
|
||||||
|
export { parseNerveConfig } from "./config.js";
|
||||||
|
export type { KnowledgeConfig } from "./config.js";
|
||||||
|
export { parseKnowledgeYaml } from "./config.js";
|
||||||
|
export { isPlainRecord } from "./util.js";
|
||||||
|
export { parseSenseTrigger } from "./sense.js";
|
||||||
|
export { isSenseInfo, isWorkflowStatus } from "./daemon.js";
|
||||||
|
export type { WorkflowStatus, HealthInfo, DaemonIpcTriggerWorkflowRequest, DaemonIpcTriggerSenseRequest, DaemonIpcListSensesRequest, DaemonIpcKillWorkflowRequest, DaemonIpcListWorkflowsRequest, DaemonIpcHealthRequest, DaemonIpcRequest, DaemonIpcTriggerOkResponse, DaemonIpcErrorResponse, DaemonIpcTriggerResponse, DaemonIpcListSensesResponse, DaemonIpcListWorkflowsResponse, DaemonIpcHealthResponse, DaemonIpcResponse, } from "./daemon.js";
|
||||||
|
export { parseDaemonIpcRequest } from "./daemon.js";
|
||||||
|
export type { DaemonTransport, DaemonTransportTriggerResult, DaemonTransportWorkflowLaunch, } from "./daemon.js";
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,WAAW,EACX,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,WAAW,EACX,aAAa,EACb,WAAW,EACX,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,YAAY,EACV,eAAe,EACf,UAAU,EACV,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,aAAa,EACb,eAAe,EACf,OAAO,EACP,QAAQ,EACR,gBAAgB,EAChB,SAAS,EACT,kBAAkB,GACnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAC1E,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,YAAY,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACL,eAAe,EACf,SAAS,EACT,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC5D,YAAY,EACV,cAAc,EACd,UAAU,EACV,+BAA+B,EAC/B,4BAA4B,EAC5B,0BAA0B,EAC1B,4BAA4B,EAC5B,6BAA6B,EAC7B,sBAAsB,EACtB,gBAAgB,EAChB,0BAA0B,EAC1B,sBAAsB,EACtB,wBAAwB,EACxB,2BAA2B,EAC3B,8BAA8B,EAC9B,uBAAuB,EACvB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EACV,eAAe,EACf,4BAA4B,EAC5B,6BAA6B,GAC9B,MAAM,aAAa,CAAC"}
|
||||||
@@ -25,8 +25,8 @@ export type {
|
|||||||
ModeratorContext,
|
ModeratorContext,
|
||||||
Moderator,
|
Moderator,
|
||||||
WorkflowDefinition,
|
WorkflowDefinition,
|
||||||
} from "@uncaged/workflow";
|
} from "@uncaged/workflow/public-types";
|
||||||
export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "@uncaged/workflow";
|
export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "@uncaged/workflow/public-types";
|
||||||
export type { Schema, ExtractFn } from "./agent.js";
|
export type { Schema, ExtractFn } from "./agent.js";
|
||||||
export { ExtractError } from "./agent.js";
|
export { ExtractError } from "./agent.js";
|
||||||
export type { Result } from "./util.js";
|
export type { Result } from "./util.js";
|
||||||
|
|||||||
Vendored
+39
@@ -0,0 +1,39 @@
|
|||||||
|
import type { SenseConfig, SenseTrigger } from "./config.js";
|
||||||
|
import { type Result } from "./util.js";
|
||||||
|
/** Runtime metadata for a sense (e.g. daemon list-senses IPC). */
|
||||||
|
export type SenseInfo = {
|
||||||
|
name: string;
|
||||||
|
group: string;
|
||||||
|
throttle: number | null;
|
||||||
|
timeout: number | null;
|
||||||
|
/** Declarative schedule (`interval` / `on`) for this sense (derived from nerve.yaml). */
|
||||||
|
triggers: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* The function signature every sense `src/index.ts` must export as a named
|
||||||
|
* `compute` export.
|
||||||
|
*
|
||||||
|
* Pure: no DB, no peers.
|
||||||
|
* Returns the next sense state and an optional trigger (`trigger: null` means no side effect).
|
||||||
|
*/
|
||||||
|
export type SenseComputeFn<S = unknown> = (state: S) => Promise<{
|
||||||
|
state: S;
|
||||||
|
trigger: SenseTrigger | null;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* The full shape a sense module (`src/index.ts`) must export.
|
||||||
|
*/
|
||||||
|
export type SenseModule<S = unknown> = {
|
||||||
|
compute: SenseComputeFn<S>;
|
||||||
|
initialState: S;
|
||||||
|
};
|
||||||
|
/** Human-readable label for a sense schedule (`interval` and/or `on`). */
|
||||||
|
export declare function labelSenseTrigger(slice: Pick<SenseConfig, "interval" | "on">): string;
|
||||||
|
/**
|
||||||
|
* Human-readable trigger labels for a sense from its `SenseConfig.interval` / `.on`.
|
||||||
|
* Returns an empty array when the sense is missing or has no schedule.
|
||||||
|
*/
|
||||||
|
export declare function senseTriggerLabels(senseName: string, senses: Record<string, SenseConfig>): string[];
|
||||||
|
/** Validates `{ command: string }` from Sense compute or IPC (`trigger` field). */
|
||||||
|
export declare function parseSenseTrigger(value: unknown): Result<SenseTrigger>;
|
||||||
|
//# sourceMappingURL=sense.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"sense.d.ts","sourceRoot":"","sources":["sense.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,KAAK,MAAM,EAA0B,MAAM,WAAW,CAAC;AAEhE,kEAAkE;AAClE,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,yFAAyF;IACzF,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,GAAG,OAAO,IAAI,CACxC,KAAK,EAAE,CAAC,KACL,OAAO,CAAC;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,OAAO,IAAI;IACrC,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IAC3B,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC;AAYF,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,IAAI,CAAC,GAAG,MAAM,CAYrF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAClC,MAAM,EAAE,CAKV;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC,CActE"}
|
||||||
Vendored
+66
@@ -0,0 +1,66 @@
|
|||||||
|
export type Result<T, E = Error> = {
|
||||||
|
ok: true;
|
||||||
|
value: T;
|
||||||
|
} | {
|
||||||
|
ok: false;
|
||||||
|
error: E;
|
||||||
|
};
|
||||||
|
/** Compatible with `process.env` for `child_process.spawn`. */
|
||||||
|
export type SpawnEnv = Record<string, string | undefined>;
|
||||||
|
export type SpawnResult = {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
exitCode: number;
|
||||||
|
/** OS signal name (e.g. `"SIGTERM"`) when terminated by signal; otherwise `null`. */
|
||||||
|
signal: string | null;
|
||||||
|
};
|
||||||
|
export type SpawnError = {
|
||||||
|
kind: "non_zero_exit";
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
exitCode: number;
|
||||||
|
signal: string | null;
|
||||||
|
} | {
|
||||||
|
kind: "timeout";
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
} | {
|
||||||
|
kind: "spawn_failed";
|
||||||
|
message: string;
|
||||||
|
} | {
|
||||||
|
kind: "aborted";
|
||||||
|
};
|
||||||
|
export type SpawnSafeOptions = {
|
||||||
|
cwd: string | null;
|
||||||
|
/** When null, merges {@link nerveCommandEnv} over `process.env`. When set, merges over that default. */
|
||||||
|
env: SpawnEnv | null;
|
||||||
|
timeoutMs: number | null;
|
||||||
|
dryRun: boolean;
|
||||||
|
/** When non-null, child is terminated on abort; if `timeoutMs` is also null, no internal wall-clock timer is used. */
|
||||||
|
abortSignal: AbortSignal | null;
|
||||||
|
};
|
||||||
|
type SpawnSafeOptionsInput = SpawnSafeOptions | Omit<SpawnSafeOptions, "dryRun">;
|
||||||
|
export declare function ok<T>(value: T): Result<T, never>;
|
||||||
|
export declare function err<E = Error>(error: E): Result<never, E>;
|
||||||
|
/**
|
||||||
|
* Narrows `unknown` to a plain JSON-style object (not null, not array).
|
||||||
|
* Use after `JSON.parse` / YAML / IPC when validating structure field-by-field.
|
||||||
|
*/
|
||||||
|
export declare function isPlainRecord(value: unknown): value is Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* Parse a duration string such as `5s`, `10m`, `1h` to milliseconds.
|
||||||
|
* Used by `parseNerveConfig` sense/workflow duration fields.
|
||||||
|
*/
|
||||||
|
export declare function parseDurationStringToMs(value: string): Result<number>;
|
||||||
|
/**
|
||||||
|
* PATH and PNPM_HOME for running `pnpm` and `nerve` from workflow roles.
|
||||||
|
* Uses the pnpm store home only (no npm user bin); binaries must resolve via PATH.
|
||||||
|
*/
|
||||||
|
export declare function nerveCommandEnv(): SpawnEnv;
|
||||||
|
/**
|
||||||
|
* Spawn a process with `shell: false` (argv only), default {@link nerveCommandEnv}, and optional timeout.
|
||||||
|
* Returns `ok` only when the process exits with code 0.
|
||||||
|
*/
|
||||||
|
export declare function spawnSafe(command: string, args: ReadonlyArray<string>, options: SpawnSafeOptionsInput): Promise<Result<SpawnResult, SpawnError>>;
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=util.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["util.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAEpF,+DAA+D;AAC/D,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE1D,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAClB;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB,GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAExB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,wGAAwG;IACxG,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,sHAAsH;IACtH,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;CACjC,CAAC;AAEF,KAAK,qBAAqB,GAAG,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;AAIjF,wBAAgB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAEhD;AAED,wBAAgB,GAAG,CAAC,CAAC,GAAG,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAEzD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE9E;AAUD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAMrE;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,QAAQ,CAQ1C;AAgCD;;;GAGG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAoG1C"}
|
||||||
@@ -22,10 +22,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
||||||
"build": "rslib build",
|
"build": "rslib build",
|
||||||
"pretest": "pnpm --filter @uncaged/nerve-core run build",
|
"pretest": "pnpm --filter @uncaged/workflow run build:public-types && pnpm --filter @uncaged/nerve-core run build && pnpm --filter @uncaged/nerve-store run build && pnpm --filter @uncaged/workflow run build",
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@uncaged/workflow": "workspace:*",
|
||||||
"@uncaged/nerve-core": "workspace:*",
|
"@uncaged/nerve-core": "workspace:*",
|
||||||
"@uncaged/nerve-store": "workspace:*",
|
"@uncaged/nerve-store": "workspace:*",
|
||||||
"yaml": "^2.8.3"
|
"yaml": "^2.8.3"
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export default defineConfig({
|
|||||||
entry: {
|
entry: {
|
||||||
index: "src/index.ts",
|
index: "src/index.ts",
|
||||||
"sense-worker": "src/sense-worker.ts",
|
"sense-worker": "src/sense-worker.ts",
|
||||||
"workflow-worker": "src/workflow-worker.ts",
|
|
||||||
"experimental-warning-suppression": "src/experimental-warning-suppression.ts",
|
"experimental-warning-suppression": "src/experimental-warning-suppression.ts",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ vi.mock("node:child_process", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { createWorkflowManager } = await import("../workflow-manager.js");
|
const { createWorkflowManager } = await import("@uncaged/workflow");
|
||||||
|
|
||||||
function makeConfig(workflows: Record<string, WorkflowConfig> = {}): NerveConfig {
|
function makeConfig(workflows: Record<string, WorkflowConfig> = {}): NerveConfig {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ vi.mock("node:child_process", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { createWorkflowManager } = await import("../workflow-manager.js");
|
const { createWorkflowManager } = await import("@uncaged/workflow");
|
||||||
const { createKernel } = await import("../kernel.js");
|
const { createKernel } = await import("../kernel.js");
|
||||||
|
|
||||||
function makeWfConfig(workflows: Record<string, WorkflowConfig> = {}): NerveConfig {
|
function makeWfConfig(workflows: Record<string, WorkflowConfig> = {}): NerveConfig {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { createWorkerRuntime } from "../worker-runtime.js";
|
import { createWorkerRuntime } from "@uncaged/workflow";
|
||||||
|
|
||||||
const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
|
const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), "fixtures");
|
||||||
const echoWorkerPath = join(fixturesDir, "echo-worker.js");
|
const echoWorkerPath = join(fixturesDir, "echo-worker.js");
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ vi.mock("node:child_process", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Import after mock is set up
|
// Import after mock is set up
|
||||||
const { createWorkflowManager } = await import("../workflow-manager.js");
|
const { createWorkflowManager } = await import("@uncaged/workflow");
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { HealthInfo, SenseInfo, WorkflowStatus } from "@uncaged/nerve-core";
|
import type { HealthInfo, SenseInfo, WorkflowStatus } from "@uncaged/nerve-core";
|
||||||
|
|
||||||
import type { WorkflowManager } from "./workflow-manager.js";
|
import type { WorkflowManager } from "@uncaged/workflow";
|
||||||
|
|
||||||
export type DaemonHandlerBundle = {
|
export type DaemonHandlerBundle = {
|
||||||
health: () => HealthInfo;
|
health: () => HealthInfo;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type {
|
|||||||
ResumeThreadMessage,
|
ResumeThreadMessage,
|
||||||
ThreadEventMessage,
|
ThreadEventMessage,
|
||||||
WorkflowErrorMessage,
|
WorkflowErrorMessage,
|
||||||
ThreadWorkflowMessageMessage,
|
ThreadWorkflowMessage,
|
||||||
} from "./ipc.js";
|
} from "./ipc.js";
|
||||||
|
|
||||||
export { loadSenseModule, executeCompute, readState, writeState } from "./sense-runtime.js";
|
export { loadSenseModule, executeCompute, readState, writeState } from "./sense-runtime.js";
|
||||||
@@ -45,5 +45,5 @@ export type {
|
|||||||
WorkflowRunStatus,
|
WorkflowRunStatus,
|
||||||
} from "@uncaged/nerve-store";
|
} from "@uncaged/nerve-store";
|
||||||
|
|
||||||
export { createWorkflowManager } from "./workflow-manager.js";
|
export { createWorkflowManager } from "@uncaged/workflow";
|
||||||
export type { WorkflowManager } from "./workflow-manager.js";
|
export type { WorkflowManager } from "@uncaged/workflow";
|
||||||
|
|||||||
+40
-218
@@ -1,10 +1,30 @@
|
|||||||
/**
|
/**
|
||||||
* IPC message types for parent (kernel) ↔ sense worker communication.
|
* IPC message types for parent (kernel) ↔ sense worker communication.
|
||||||
* Protocol per RFC §5.2: hub-and-spoke, all messages through engine.
|
* Protocol per RFC §5.2: hub-and-spoke, all messages through engine.
|
||||||
|
*
|
||||||
|
* Workflow worker IPC types and parsers live in `@uncaged/workflow`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Result, SenseTrigger } from "@uncaged/nerve-core";
|
import type { Result, SenseTrigger } from "@uncaged/nerve-core";
|
||||||
import { err, isPlainRecord, ok, parseSenseTrigger } from "@uncaged/nerve-core";
|
import { err, isPlainRecord, ok, parseSenseTrigger } from "@uncaged/nerve-core";
|
||||||
|
import type {
|
||||||
|
KillThreadMessage,
|
||||||
|
ResumeThreadMessage,
|
||||||
|
StartThreadMessage,
|
||||||
|
ThreadEventMessage,
|
||||||
|
ThreadWorkflowMessage,
|
||||||
|
WorkflowErrorMessage,
|
||||||
|
} from "@uncaged/workflow";
|
||||||
|
import { parseWorkflowParentMessage, parseWorkflowWorkerToParentMessage } from "@uncaged/workflow";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
KillThreadMessage,
|
||||||
|
ResumeThreadMessage,
|
||||||
|
StartThreadMessage,
|
||||||
|
ThreadEventMessage,
|
||||||
|
ThreadWorkflowMessage,
|
||||||
|
WorkflowErrorMessage,
|
||||||
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
/** Parent → Worker: trigger one compute cycle for a sense */
|
/** Parent → Worker: trigger one compute cycle for a sense */
|
||||||
export type ComputeMessage = {
|
export type ComputeMessage = {
|
||||||
@@ -22,40 +42,6 @@ export type HealthRequestMessage = {
|
|||||||
type: "health-request";
|
type: "health-request";
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Workflow IPC messages (RFC-002 §5.2)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Parent → Workflow Worker: start a new thread */
|
|
||||||
export type StartThreadMessage = {
|
|
||||||
type: "start-thread";
|
|
||||||
runId: string;
|
|
||||||
workflow: string;
|
|
||||||
prompt: string;
|
|
||||||
/** Safety-valve: max moderator rounds for this thread (engine launch parameter). */
|
|
||||||
maxRounds: number;
|
|
||||||
/** When true, roles may skip side effects (thread-level hint on the start frame). */
|
|
||||||
dryRun: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Parent → Workflow Worker: resume an existing thread after crash recovery */
|
|
||||||
export type ResumeThreadMessage = {
|
|
||||||
type: "resume-thread";
|
|
||||||
runId: string;
|
|
||||||
/** Serialised WorkflowMessage history to rebuild chain (must begin with `__start__`). */
|
|
||||||
messages: Array<{ role: string; content: string; meta: unknown; timestamp: number }>;
|
|
||||||
/** Safety-valve: max moderator rounds for this thread. */
|
|
||||||
maxRounds: number;
|
|
||||||
/** Thread-level dry-run hint (aligns with persisted `__start__` meta when replaying). */
|
|
||||||
dryRun: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Parent → Workflow Worker: kill a specific running thread */
|
|
||||||
export type KillThreadMessage = {
|
|
||||||
type: "kill-thread";
|
|
||||||
runId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Union of all messages the parent sends to a worker */
|
/** Union of all messages the parent sends to a worker */
|
||||||
export type ParentToWorkerMessage =
|
export type ParentToWorkerMessage =
|
||||||
| ComputeMessage
|
| ComputeMessage
|
||||||
@@ -92,46 +78,6 @@ export type HealthResponseMessage = {
|
|||||||
inFlightCount: number;
|
inFlightCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Workflow Worker → Parent messages (RFC-002 §5.2)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Valid lifecycle event types for a workflow thread. */
|
|
||||||
export type ThreadEventType =
|
|
||||||
| "queued"
|
|
||||||
| "started"
|
|
||||||
| "step_complete"
|
|
||||||
| "completed"
|
|
||||||
| "failed"
|
|
||||||
| "killed";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Workflow Worker → Parent: a thread lifecycle event.
|
|
||||||
*/
|
|
||||||
export type ThreadEventMessage = {
|
|
||||||
type: "thread-event";
|
|
||||||
runId: string;
|
|
||||||
eventType: ThreadEventType;
|
|
||||||
payload: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Workflow Worker → Parent: a thread encountered an unrecoverable error. */
|
|
||||||
export type WorkflowErrorMessage = {
|
|
||||||
type: "workflow-error";
|
|
||||||
runId: string;
|
|
||||||
error: string;
|
|
||||||
/** Exit code conveying the failure reason (1=role error, 2=maxRounds exhausted). */
|
|
||||||
exitCode: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Workflow Worker → Parent: a WorkflowMessage produced by a role (for crash recovery). */
|
|
||||||
export type ThreadWorkflowMessageMessage = {
|
|
||||||
type: "thread-workflow-message";
|
|
||||||
runId: string;
|
|
||||||
/** The WorkflowMessage produced by the role — persisted for crash recovery. */
|
|
||||||
message: { role: string; content: string; meta: unknown; timestamp: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Union of all messages a worker sends to the parent */
|
/** Union of all messages a worker sends to the parent */
|
||||||
export type WorkerToParentMessage =
|
export type WorkerToParentMessage =
|
||||||
| ComputeResultMessage
|
| ComputeResultMessage
|
||||||
@@ -140,7 +86,7 @@ export type WorkerToParentMessage =
|
|||||||
| HealthResponseMessage
|
| HealthResponseMessage
|
||||||
| ThreadEventMessage
|
| ThreadEventMessage
|
||||||
| WorkflowErrorMessage
|
| WorkflowErrorMessage
|
||||||
| ThreadWorkflowMessageMessage;
|
| ThreadWorkflowMessage;
|
||||||
|
|
||||||
const PARENT_MSG_TYPES = new Set([
|
const PARENT_MSG_TYPES = new Set([
|
||||||
"compute",
|
"compute",
|
||||||
@@ -151,24 +97,6 @@ const PARENT_MSG_TYPES = new Set([
|
|||||||
"kill-thread",
|
"kill-thread",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function validateStartThreadMsg(obj: Record<string, unknown>): string | null {
|
|
||||||
if (typeof obj.runId !== "string") return "'start-thread' message missing string 'runId'";
|
|
||||||
if (typeof obj.workflow !== "string") return "'start-thread' message missing string 'workflow'";
|
|
||||||
if (typeof obj.prompt !== "string") return "'start-thread' message missing string 'prompt'";
|
|
||||||
if (typeof obj.maxRounds !== "number") return "'start-thread' message missing number 'maxRounds'";
|
|
||||||
if (typeof obj.dryRun !== "boolean") return "'start-thread' message missing boolean 'dryRun'";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateResumeThreadMsg(obj: Record<string, unknown>): string | null {
|
|
||||||
if (typeof obj.runId !== "string") return "'resume-thread' message missing string 'runId'";
|
|
||||||
if (!Array.isArray(obj.messages)) return "'resume-thread' message missing 'messages' array";
|
|
||||||
if (typeof obj.maxRounds !== "number")
|
|
||||||
return "'resume-thread' message missing number 'maxRounds'";
|
|
||||||
if (typeof obj.dryRun !== "boolean") return "'resume-thread' message missing boolean 'dryRun'";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseParentCompute(obj: Record<string, unknown>): Result<ParentToWorkerMessage> {
|
function parseParentCompute(obj: Record<string, unknown>): Result<ParentToWorkerMessage> {
|
||||||
if (typeof obj.sense !== "string") {
|
if (typeof obj.sense !== "string") {
|
||||||
return err(new Error("IPC 'compute' message missing string 'sense' field"));
|
return err(new Error("IPC 'compute' message missing string 'sense' field"));
|
||||||
@@ -176,40 +104,6 @@ function parseParentCompute(obj: Record<string, unknown>): Result<ParentToWorker
|
|||||||
return ok({ type: "compute", sense: obj.sense });
|
return ok({ type: "compute", sense: obj.sense });
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseParentStartThread(obj: Record<string, unknown>): Result<ParentToWorkerMessage> {
|
|
||||||
const errMsg = validateStartThreadMsg(obj);
|
|
||||||
if (errMsg !== null) return err(new Error(errMsg));
|
|
||||||
// Field types are validated above; `Record<string, unknown>` values stay `unknown` to TypeScript.
|
|
||||||
return ok({
|
|
||||||
type: "start-thread",
|
|
||||||
runId: obj.runId,
|
|
||||||
workflow: obj.workflow,
|
|
||||||
prompt: obj.prompt,
|
|
||||||
maxRounds: obj.maxRounds,
|
|
||||||
dryRun: obj.dryRun,
|
|
||||||
} as StartThreadMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseParentResumeThread(obj: Record<string, unknown>): Result<ParentToWorkerMessage> {
|
|
||||||
const errMsg = validateResumeThreadMsg(obj);
|
|
||||||
if (errMsg !== null) return err(new Error(errMsg));
|
|
||||||
// Elements are validated as plain objects by the kernel; trust the wire shape here.
|
|
||||||
return ok({
|
|
||||||
type: "resume-thread",
|
|
||||||
runId: obj.runId,
|
|
||||||
messages: obj.messages as ResumeThreadMessage["messages"],
|
|
||||||
maxRounds: obj.maxRounds,
|
|
||||||
dryRun: obj.dryRun,
|
|
||||||
} as ResumeThreadMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseParentKillThread(obj: Record<string, unknown>): Result<ParentToWorkerMessage> {
|
|
||||||
if (typeof obj.runId !== "string") {
|
|
||||||
return err(new Error("'kill-thread' message missing string 'runId'"));
|
|
||||||
}
|
|
||||||
return ok({ type: "kill-thread", runId: obj.runId } as KillThreadMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Validate and parse an unknown IPC message received from the parent process. */
|
/** Validate and parse an unknown IPC message received from the parent process. */
|
||||||
export function parseParentMessage(raw: unknown): Result<ParentToWorkerMessage> {
|
export function parseParentMessage(raw: unknown): Result<ParentToWorkerMessage> {
|
||||||
if (!isPlainRecord(raw)) {
|
if (!isPlainRecord(raw)) {
|
||||||
@@ -230,11 +124,14 @@ export function parseParentMessage(raw: unknown): Result<ParentToWorkerMessage>
|
|||||||
case "health-request":
|
case "health-request":
|
||||||
return ok({ type: "health-request" });
|
return ok({ type: "health-request" });
|
||||||
case "start-thread":
|
case "start-thread":
|
||||||
return parseParentStartThread(obj);
|
|
||||||
case "resume-thread":
|
case "resume-thread":
|
||||||
return parseParentResumeThread(obj);
|
case "kill-thread": {
|
||||||
case "kill-thread":
|
const wf = parseWorkflowParentMessage(raw);
|
||||||
return parseParentKillThread(obj);
|
if (!wf.ok) {
|
||||||
|
return wf;
|
||||||
|
}
|
||||||
|
return ok(wf.value as ParentToWorkerMessage);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return err(new Error(`Unhandled IPC message type: "${obj.type}"`));
|
return err(new Error(`Unhandled IPC message type: "${obj.type}"`));
|
||||||
}
|
}
|
||||||
@@ -299,55 +196,11 @@ function parseHealthResponseMsg(obj: Record<string, unknown>): Result<WorkerToPa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isThreadEventType(value: string): value is ThreadEventType {
|
const WORKFLOW_WORKER_MSG_TYPES = new Set([
|
||||||
switch (value) {
|
"thread-event",
|
||||||
case "queued":
|
"workflow-error",
|
||||||
case "started":
|
"thread-workflow-message",
|
||||||
case "step_complete":
|
]);
|
||||||
case "completed":
|
|
||||||
case "failed":
|
|
||||||
case "killed":
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseThreadEventMsg(obj: Record<string, unknown>): Result<WorkerToParentMessage> {
|
|
||||||
if (typeof obj.runId !== "string") {
|
|
||||||
return err(new Error("Worker 'thread-event' message missing string 'runId' field"));
|
|
||||||
}
|
|
||||||
if (typeof obj.eventType !== "string" || !isThreadEventType(obj.eventType)) {
|
|
||||||
return err(
|
|
||||||
new Error(`Worker 'thread-event' message has invalid 'eventType': "${obj.eventType}"`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!("payload" in obj)) {
|
|
||||||
return err(new Error("Worker 'thread-event' message missing 'payload' field"));
|
|
||||||
}
|
|
||||||
return ok({
|
|
||||||
type: "thread-event",
|
|
||||||
runId: obj.runId,
|
|
||||||
eventType: obj.eventType,
|
|
||||||
payload: obj.payload,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseWorkflowErrorMsg(obj: Record<string, unknown>): Result<WorkerToParentMessage> {
|
|
||||||
if (typeof obj.runId !== "string") {
|
|
||||||
return err(new Error("Worker 'workflow-error' message missing string 'runId' field"));
|
|
||||||
}
|
|
||||||
if (typeof obj.error !== "string") {
|
|
||||||
return err(new Error("Worker 'workflow-error' message missing string 'error' field"));
|
|
||||||
}
|
|
||||||
const exitCode = typeof obj.exitCode === "number" ? obj.exitCode : 1;
|
|
||||||
return ok({
|
|
||||||
type: "workflow-error",
|
|
||||||
runId: obj.runId,
|
|
||||||
error: obj.error,
|
|
||||||
exitCode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const WORKER_MSG_TYPES = new Set([
|
const WORKER_MSG_TYPES = new Set([
|
||||||
"compute-result",
|
"compute-result",
|
||||||
@@ -359,41 +212,6 @@ const WORKER_MSG_TYPES = new Set([
|
|||||||
"thread-workflow-message",
|
"thread-workflow-message",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function parseThreadWorkflowMessageMsg(
|
|
||||||
obj: Record<string, unknown>,
|
|
||||||
): Result<WorkerToParentMessage> {
|
|
||||||
if (typeof obj.runId !== "string") {
|
|
||||||
return err(new Error("Worker 'thread-workflow-message' missing string 'runId' field"));
|
|
||||||
}
|
|
||||||
if (!isPlainRecord(obj.message)) {
|
|
||||||
return err(new Error("Worker 'thread-workflow-message' missing object 'message' field"));
|
|
||||||
}
|
|
||||||
const msg = obj.message;
|
|
||||||
if (typeof msg.role !== "string") {
|
|
||||||
return err(new Error("Worker 'thread-workflow-message' message missing string 'role' field"));
|
|
||||||
}
|
|
||||||
if (typeof msg.content !== "string") {
|
|
||||||
return err(
|
|
||||||
new Error("Worker 'thread-workflow-message' message missing string 'content' field"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (typeof msg.timestamp !== "number") {
|
|
||||||
return err(
|
|
||||||
new Error("Worker 'thread-workflow-message' message missing number 'timestamp' field"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return ok({
|
|
||||||
type: "thread-workflow-message",
|
|
||||||
runId: obj.runId,
|
|
||||||
message: {
|
|
||||||
role: msg.role,
|
|
||||||
content: msg.content,
|
|
||||||
meta: "meta" in msg ? msg.meta : undefined,
|
|
||||||
timestamp: msg.timestamp,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Validate and parse an unknown IPC message received from a worker process. */
|
/** Validate and parse an unknown IPC message received from a worker process. */
|
||||||
export function parseWorkerMessage(raw: unknown): Result<WorkerToParentMessage> {
|
export function parseWorkerMessage(raw: unknown): Result<WorkerToParentMessage> {
|
||||||
if (!isPlainRecord(raw)) {
|
if (!isPlainRecord(raw)) {
|
||||||
@@ -406,11 +224,15 @@ export function parseWorkerMessage(raw: unknown): Result<WorkerToParentMessage>
|
|||||||
if (!WORKER_MSG_TYPES.has(obj.type)) {
|
if (!WORKER_MSG_TYPES.has(obj.type)) {
|
||||||
return err(new Error(`Unknown worker IPC message type: "${obj.type}"`));
|
return err(new Error(`Unknown worker IPC message type: "${obj.type}"`));
|
||||||
}
|
}
|
||||||
|
if (WORKFLOW_WORKER_MSG_TYPES.has(obj.type)) {
|
||||||
|
const wf = parseWorkflowWorkerToParentMessage(raw);
|
||||||
|
if (!wf.ok) {
|
||||||
|
return wf;
|
||||||
|
}
|
||||||
|
return ok(wf.value as WorkerToParentMessage);
|
||||||
|
}
|
||||||
if (obj.type === "compute-result") return parseComputeResultMsg(obj);
|
if (obj.type === "compute-result") return parseComputeResultMsg(obj);
|
||||||
if (obj.type === "error") return parseErrorMsg(obj);
|
if (obj.type === "error") return parseErrorMsg(obj);
|
||||||
if (obj.type === "health-response") return parseHealthResponseMsg(obj);
|
if (obj.type === "health-response") return parseHealthResponseMsg(obj);
|
||||||
if (obj.type === "thread-event") return parseThreadEventMsg(obj);
|
|
||||||
if (obj.type === "workflow-error") return parseWorkflowErrorMsg(obj);
|
|
||||||
if (obj.type === "thread-workflow-message") return parseThreadWorkflowMessageMsg(obj);
|
|
||||||
return ok({ type: "ready" });
|
return ok({ type: "ready" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { NerveConfig } from "@uncaged/nerve-core";
|
|||||||
import { parseNerveConfig } from "@uncaged/nerve-core";
|
import { parseNerveConfig } from "@uncaged/nerve-core";
|
||||||
|
|
||||||
import type { LogStore } from "@uncaged/nerve-store";
|
import type { LogStore } from "@uncaged/nerve-store";
|
||||||
import type { WorkflowManager } from "./workflow-manager.js";
|
import type { WorkflowManager } from "@uncaged/workflow";
|
||||||
|
|
||||||
export type KernelFileWatchDeps = {
|
export type KernelFileWatchDeps = {
|
||||||
nerveRoot: string;
|
nerveRoot: string;
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ import {
|
|||||||
import { createSenseScheduler } from "./sense-scheduler.js";
|
import { createSenseScheduler } from "./sense-scheduler.js";
|
||||||
import type { SenseScheduler } from "./sense-scheduler.js";
|
import type { SenseScheduler } from "./sense-scheduler.js";
|
||||||
import { createSenseWorkerPool, resolveWorkerScript } from "./worker-pool.js";
|
import { createSenseWorkerPool, resolveWorkerScript } from "./worker-pool.js";
|
||||||
import { createWorkflowManager } from "./workflow-manager.js";
|
import { createWorkflowManager } from "@uncaged/workflow";
|
||||||
import type { WorkflowManager } from "./workflow-manager.js";
|
import type { WorkflowManager } from "@uncaged/workflow";
|
||||||
|
|
||||||
export type KernelHealth = {
|
export type KernelHealth = {
|
||||||
uptime: number;
|
uptime: number;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
createWorkerRuntime,
|
createWorkerRuntime,
|
||||||
formatCapturedStderrTail,
|
formatCapturedStderrTail,
|
||||||
formatChildExitSummary,
|
formatChildExitSummary,
|
||||||
} from "./worker-runtime.js";
|
} from "@uncaged/workflow";
|
||||||
|
|
||||||
export function resolveWorkerScript(): string {
|
export function resolveWorkerScript(): string {
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
|||||||
Vendored
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* CAS blob store — sha256 content-addressable files under `data/blobs/`.
|
||||||
|
*
|
||||||
|
* Layout: `<root>/<2-hex-shard>/<62-hex-rest>` (RFC-001 §8).
|
||||||
|
*/
|
||||||
|
export type BlobStore = {
|
||||||
|
/** Persist UTF-8 or raw bytes; returns lowercase hex sha256. Idempotent for identical content. */
|
||||||
|
write: (content: string | Uint8Array | Buffer) => string;
|
||||||
|
/** Returns bytes or null if the hash is invalid or no blob exists. Verifies digest matches path. */
|
||||||
|
read: (hash: string) => Buffer | null;
|
||||||
|
/** True when hash is well-formed and the blob file is present. */
|
||||||
|
exists: (hash: string) => boolean;
|
||||||
|
};
|
||||||
|
/** @returns normalized lowercase hex or null if not a valid sha256 hex string */
|
||||||
|
export declare function normalizeBlobHash(hash: string): string | null;
|
||||||
|
export declare function createBlobStore(blobsRoot: string): BlobStore;
|
||||||
|
//# sourceMappingURL=blob-store.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"blob-store.d.ts","sourceRoot":"","sources":["blob-store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAgBH,MAAM,MAAM,SAAS,GAAG;IACtB,kGAAkG;IAClG,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,KAAK,MAAM,CAAC;IACzD,oGAAoG;IACpG,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACtC,kEAAkE;IAClE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC,CAAC;AAYF,iFAAiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK7D;AAiBD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CA2C5D"}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @uncaged/nerve-store — append-only log storage, cold-archive helpers, CAS blob store.
|
||||||
|
*/
|
||||||
|
export * from "./blob-store.js";
|
||||||
|
export * from "./log-archive.js";
|
||||||
|
export { createLogStore } from "./log-store.js";
|
||||||
|
export type { GetThreadRoundsParams, LogEntry, LogQuery, LogStore, ThreadRoundRow, WorkflowRun, WorkflowRunStatus, } from "./log-store.js";
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,YAAY,EACV,qBAAqB,EACrB,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,WAAW,EACX,iBAAiB,GAClB,MAAM,gBAAgB,CAAC"}
|
||||||
Vendored
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/** Log cold-archive helpers (RFC-001 §5.4) — UTC calendar days, JSONL export. */
|
||||||
|
export declare const LOG_ARCHIVE_META_KEY = "archived_up_to";
|
||||||
|
export declare const DEFAULT_LOG_RETENTION_MS: number;
|
||||||
|
export type ArchiveLogsOptions = {
|
||||||
|
/** Wall clock for retention boundary (default: `Date.now()`). */
|
||||||
|
now?: number;
|
||||||
|
/** Run `VACUUM` after archiving (outside the per-day transaction). */
|
||||||
|
vacuum?: boolean;
|
||||||
|
/** Max UTC days to process in one call (default: unlimited). */
|
||||||
|
maxDays?: number;
|
||||||
|
/** Override default 30-day retention (tests). */
|
||||||
|
retentionMs?: number;
|
||||||
|
};
|
||||||
|
export type ArchiveLogsDayResult = {
|
||||||
|
day: string;
|
||||||
|
rowCount: number;
|
||||||
|
filePath: string;
|
||||||
|
};
|
||||||
|
export type ArchiveLogsResult = {
|
||||||
|
days: ArchiveLogsDayResult[];
|
||||||
|
vacuumed: boolean;
|
||||||
|
};
|
||||||
|
export declare function utcDateStringFromMs(ms: number): string;
|
||||||
|
export declare function assertValidUtcDay(day: string): void;
|
||||||
|
export declare function utcDayStartMs(day: string): number;
|
||||||
|
export declare function utcDayEndExclusiveMs(day: string): number;
|
||||||
|
export declare function prevUtcDay(day: string): string;
|
||||||
|
export declare function nextUtcDay(day: string): string;
|
||||||
|
/** Last UTC calendar day D such that the exclusive end of D is ≤ boundaryMs. */
|
||||||
|
export declare function lastArchivableUtcDay(boundaryMs: number): string;
|
||||||
|
export declare function compareIsoDays(a: string, b: string): number;
|
||||||
|
//# sourceMappingURL=log-archive.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"log-archive.d.ts","sourceRoot":"","sources":["log-archive.ts"],"names":[],"mappings":"AAAA,iFAAiF;AAEjF,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAErD,eAAO,MAAM,wBAAwB,QAAkB,CAAC;AAExD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,oBAAoB,EAAE,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEtD;AAiBD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,gFAAgF;AAChF,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAI3D"}
|
||||||
Vendored
+134
@@ -0,0 +1,134 @@
|
|||||||
|
/**
|
||||||
|
* Log Store — append-only structured log storage backed by SQLite.
|
||||||
|
*
|
||||||
|
* Stores system, sense-scheduler (`sense_scheduler` source), sense, and workflow log entries in a single table.
|
||||||
|
* Logs are data assets for audit/analysis — they MUST NOT feed back into scheduling or workflows as triggers.
|
||||||
|
*
|
||||||
|
* Also provides a `meta` key-value table for bookkeeping (e.g. archive watermarks).
|
||||||
|
*/
|
||||||
|
import type { ArchiveLogsOptions, ArchiveLogsResult } from "./log-archive.js";
|
||||||
|
export { LOG_ARCHIVE_META_KEY } from "./log-archive.js";
|
||||||
|
export type { ArchiveLogsDayResult, ArchiveLogsOptions, ArchiveLogsResult } from "./log-archive.js";
|
||||||
|
export type LogEntry = {
|
||||||
|
id?: number;
|
||||||
|
source: string;
|
||||||
|
type: string;
|
||||||
|
refId: string | null;
|
||||||
|
payload: string | null;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
export type LogQuery = {
|
||||||
|
source?: string;
|
||||||
|
type?: string;
|
||||||
|
refId?: string;
|
||||||
|
since?: number;
|
||||||
|
until?: number;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
export type WorkflowRunStatus = "queued" | "started" | "completed" | "failed" | "crashed" | "dropped" | "interrupted" | "killed";
|
||||||
|
/** One row in the workflow_runs materialized table. */
|
||||||
|
export type WorkflowRun = {
|
||||||
|
runId: string;
|
||||||
|
workflow: string;
|
||||||
|
status: WorkflowRunStatus;
|
||||||
|
timestamp: number;
|
||||||
|
exitCode: number | null;
|
||||||
|
};
|
||||||
|
/** One role-produced workflow-message row with 1-based round index (ROW_NUMBER over role messages only). */
|
||||||
|
export type ThreadRoundRow = {
|
||||||
|
round: number;
|
||||||
|
logId: number;
|
||||||
|
timestamp: number;
|
||||||
|
message: {
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
meta: unknown;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** Parameters for {@link LogStore.getThreadRounds}. */
|
||||||
|
export type GetThreadRoundsParams = {
|
||||||
|
/**
|
||||||
|
* Exclusive upper bound on round index (1-based among role events).
|
||||||
|
* Use `0` to include all rounds (subject to `limit`).
|
||||||
|
*/
|
||||||
|
before: number;
|
||||||
|
/** Maximum rows returned from the DB (DESC by round). */
|
||||||
|
limit: number;
|
||||||
|
};
|
||||||
|
export type LogStore = {
|
||||||
|
append: (entry: Omit<LogEntry, "id">) => LogEntry;
|
||||||
|
query: (filter?: LogQuery) => LogEntry[];
|
||||||
|
getMeta: (key: string) => string | null;
|
||||||
|
setMeta: (key: string, value: string) => void;
|
||||||
|
/**
|
||||||
|
* Append a workflow log event and atomically upsert the workflow_runs
|
||||||
|
* materialized table — both in a single SQLite transaction (RFC-002 §6.2).
|
||||||
|
*/
|
||||||
|
upsertWorkflowRun: (entry: Omit<LogEntry, "id">, run: WorkflowRun) => LogEntry;
|
||||||
|
/**
|
||||||
|
* Alias for upsertWorkflowRun — append a log entry and update workflow_runs
|
||||||
|
* in one atomic transaction.
|
||||||
|
*/
|
||||||
|
appendWithWorkflowUpdate: (entry: Omit<LogEntry, "id">, run: WorkflowRun) => LogEntry;
|
||||||
|
/** Get the current materialized state of a specific workflow run. */
|
||||||
|
getWorkflowRun: (runId: string) => WorkflowRun | null;
|
||||||
|
/**
|
||||||
|
* Get all workflow runs with status 'queued' or 'started'.
|
||||||
|
* Optionally filter by workflow name.
|
||||||
|
*/
|
||||||
|
getActiveWorkflowRuns: (workflowName?: string) => WorkflowRun[];
|
||||||
|
/**
|
||||||
|
* Get all workflow runs regardless of status, sorted by timestamp descending.
|
||||||
|
* Optionally filter by workflow name.
|
||||||
|
*/
|
||||||
|
getAllWorkflowRuns: (workflowName: string | null) => WorkflowRun[];
|
||||||
|
/**
|
||||||
|
* Get the trigger payload for a workflow run (stored in the 'started' log entry).
|
||||||
|
* Returns null if not found.
|
||||||
|
*/
|
||||||
|
getTriggerPayload: (runId: string) => unknown;
|
||||||
|
/**
|
||||||
|
* Get all workflow CommandEvents for a specific run, ordered by id ASC.
|
||||||
|
* @deprecated Use getThreadMessages for the new WorkflowMessage format.
|
||||||
|
*/
|
||||||
|
getThreadEvents: (runId: string) => Array<{
|
||||||
|
type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Get all WorkflowMessages for a specific run, ordered by id ASC.
|
||||||
|
* Used for crash recovery to rebuild the message chain.
|
||||||
|
*/
|
||||||
|
getThreadMessages: (runId: string) => Array<{
|
||||||
|
role: string;
|
||||||
|
content: string;
|
||||||
|
meta: unknown;
|
||||||
|
timestamp: number;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Count role command events for a run (excludes `thread_start`/`__start__` messages and invalid payloads).
|
||||||
|
* Round indices for {@link getThreadRounds} are 1..count in chronological order.
|
||||||
|
*/
|
||||||
|
getThreadRoundCount: (runId: string) => number;
|
||||||
|
/**
|
||||||
|
* Role rounds for agent-oriented retrieval: each row is one `thread_command_event` or
|
||||||
|
* `thread_workflow_message` whose JSON `type` is not `thread_start` and `role` is not `__start__`,
|
||||||
|
* with `round` from ROW_NUMBER() OVER (ORDER BY id ASC). No schema migration — numbering is computed in SQL.
|
||||||
|
*/
|
||||||
|
getThreadRounds: (runId: string, params: GetThreadRoundsParams) => ThreadRoundRow[];
|
||||||
|
/**
|
||||||
|
* The workflow `__start__` message for a run (if persisted), as a {@link ThreadRoundRow}
|
||||||
|
* with `round` 0 — not part of {@link getThreadRoundCount} / {@link getThreadRounds} numbering.
|
||||||
|
*/
|
||||||
|
getThreadStartMessage: (runId: string) => ThreadRoundRow | null;
|
||||||
|
/**
|
||||||
|
* Export logs older than the retention window to `data/archive/logs/YYYY-MM-DD.jsonl`,
|
||||||
|
* then delete those rows and advance `meta.archived_up_to` in one transaction per day
|
||||||
|
* (RFC-001 §5.4).
|
||||||
|
*/
|
||||||
|
archiveLogs: (options?: ArchiveLogsOptions) => ArchiveLogsResult;
|
||||||
|
close: () => void;
|
||||||
|
};
|
||||||
|
export declare function createLogStore(dbPath: string): LogStore;
|
||||||
|
//# sourceMappingURL=log-store.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"log-store.d.ts","sourceRoot":"","sources":["log-store.ts"],"names":[],"mappings":"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"}
|
||||||
@@ -8,7 +8,12 @@
|
|||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
},
|
||||||
|
"./public-types": {
|
||||||
|
"types": "./dist/public-types.d.ts",
|
||||||
|
"default": "./dist/public-types.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"files": ["dist"],
|
"files": ["dist"],
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -16,7 +21,12 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
"prepublishOnly": "bash ../../scripts/prepublish-check.sh",
|
||||||
"build": "rslib build"
|
"build": "rslib build && pnpm run build:public-types",
|
||||||
|
"build:public-types": "tsc -p tsconfig.public-types.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uncaged/nerve-core": "workspace:*",
|
||||||
|
"@uncaged/nerve-store": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rslib/core": "^0.21.3",
|
"@rslib/core": "^0.21.3",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default defineConfig({
|
|||||||
source: {
|
source: {
|
||||||
entry: {
|
entry: {
|
||||||
index: "src/index.ts",
|
index: "src/index.ts",
|
||||||
|
worker: "src/worker.ts",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Patches `process.emit` so `ExperimentalWarning` (e.g. from `node:sqlite`) is not
|
||||||
|
* forwarded to the default handler. Other warning types are unchanged.
|
||||||
|
*
|
||||||
|
* Import this module before any code that loads `node:sqlite`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const WARNING_EVENT = "warning";
|
||||||
|
const EXPERIMENTAL_WARNING_NAME = "ExperimentalWarning";
|
||||||
|
|
||||||
|
type EmitFn = typeof process.emit;
|
||||||
|
|
||||||
|
const originalEmit = process.emit.bind(process) as EmitFn;
|
||||||
|
|
||||||
|
process.emit = ((event: string | symbol, ...args: unknown[]): boolean => {
|
||||||
|
if (event === WARNING_EVENT) {
|
||||||
|
const w = args[0];
|
||||||
|
if (w instanceof Error && w.name === EXPERIMENTAL_WARNING_NAME) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Reflect.apply(originalEmit, process, [event, ...args]) as boolean;
|
||||||
|
}) as EmitFn;
|
||||||
@@ -17,3 +17,37 @@ export type {
|
|||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
export type { DropOverflowConfig, QueueOverflowConfig, WorkflowConfig } from "./config.js";
|
export type { DropOverflowConfig, QueueOverflowConfig, WorkflowConfig } from "./config.js";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
KillThreadMessage,
|
||||||
|
ResumeThreadMessage,
|
||||||
|
StartThreadMessage,
|
||||||
|
WorkflowParentToWorkerMessage,
|
||||||
|
WorkflowWorkerShutdownMessage,
|
||||||
|
ThreadEventType,
|
||||||
|
ThreadLifecycleEvent,
|
||||||
|
ThreadEventMessage,
|
||||||
|
WorkflowErrorMessage,
|
||||||
|
ThreadWorkflowMessage,
|
||||||
|
WorkflowWorkerToParentMessage,
|
||||||
|
WorkflowWorkerReadyMessage,
|
||||||
|
WorkflowChildToParentMessage,
|
||||||
|
WorkflowWorkerOutboundMessage,
|
||||||
|
} from "./ipc.js";
|
||||||
|
export {
|
||||||
|
parseWorkflowParentMessage,
|
||||||
|
parseWorkflowWorkerToParentMessage,
|
||||||
|
parseWorkflowChildMessage,
|
||||||
|
} from "./ipc.js";
|
||||||
|
|
||||||
|
export { WORKFLOW_WORKER_PATH } from "./paths.js";
|
||||||
|
|
||||||
|
export {
|
||||||
|
createWorkerRuntime,
|
||||||
|
formatCapturedStderrTail,
|
||||||
|
formatChildExitSummary,
|
||||||
|
} from "./worker-runtime.js";
|
||||||
|
export type { WorkerDrainOpts, WorkerRuntime, WorkerRuntimeConfig } from "./worker-runtime.js";
|
||||||
|
|
||||||
|
export { createWorkflowManager } from "./manager.js";
|
||||||
|
export type { WorkflowLaunchParams, WorkflowManager } from "./manager.js";
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/**
|
||||||
|
* IPC message types for parent (kernel) ↔ workflow worker communication.
|
||||||
|
* Protocol per RFC-002 §5.2.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Result } from "@uncaged/nerve-core";
|
||||||
|
import { err, isPlainRecord, ok } from "@uncaged/nerve-core";
|
||||||
|
|
||||||
|
/** Parent → Workflow Worker: start a new thread */
|
||||||
|
export type StartThreadMessage = {
|
||||||
|
type: "start-thread";
|
||||||
|
runId: string;
|
||||||
|
workflow: string;
|
||||||
|
prompt: string;
|
||||||
|
/** Safety-valve: max moderator rounds for this thread (engine launch parameter). */
|
||||||
|
maxRounds: number;
|
||||||
|
/** When true, roles may skip side effects (thread-level hint on the start frame). */
|
||||||
|
dryRun: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Parent → Workflow Worker: resume an existing thread after crash recovery */
|
||||||
|
export type ResumeThreadMessage = {
|
||||||
|
type: "resume-thread";
|
||||||
|
runId: string;
|
||||||
|
/** Serialised WorkflowMessage history to rebuild chain (must begin with `__start__`). */
|
||||||
|
messages: Array<{ role: string; content: string; meta: unknown; timestamp: number }>;
|
||||||
|
/** Safety-valve: max moderator rounds for this thread. */
|
||||||
|
maxRounds: number;
|
||||||
|
/** Thread-level dry-run hint (aligns with persisted `__start__` meta when replaying). */
|
||||||
|
dryRun: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Parent → Workflow Worker: kill a specific running thread */
|
||||||
|
export type KillThreadMessage = {
|
||||||
|
type: "kill-thread";
|
||||||
|
runId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Parent → Workflow Worker: graceful shutdown (same wire shape as sense worker). */
|
||||||
|
export type WorkflowWorkerShutdownMessage = {
|
||||||
|
type: "shutdown";
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Messages the parent sends to a workflow worker process */
|
||||||
|
export type WorkflowParentToWorkerMessage =
|
||||||
|
| StartThreadMessage
|
||||||
|
| ResumeThreadMessage
|
||||||
|
| KillThreadMessage
|
||||||
|
| WorkflowWorkerShutdownMessage;
|
||||||
|
|
||||||
|
/** Valid lifecycle event types for a workflow thread. */
|
||||||
|
export type ThreadEventType =
|
||||||
|
| "queued"
|
||||||
|
| "started"
|
||||||
|
| "step_complete"
|
||||||
|
| "completed"
|
||||||
|
| "failed"
|
||||||
|
| "killed";
|
||||||
|
|
||||||
|
/** Alias — lifecycle channel uses `ThreadEventType` values. */
|
||||||
|
export type ThreadLifecycleEvent = ThreadEventType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workflow Worker → Parent: a thread lifecycle event.
|
||||||
|
*/
|
||||||
|
export type ThreadEventMessage = {
|
||||||
|
type: "thread-event";
|
||||||
|
runId: string;
|
||||||
|
eventType: ThreadEventType;
|
||||||
|
payload: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Workflow Worker → Parent: a thread encountered an unrecoverable error. */
|
||||||
|
export type WorkflowErrorMessage = {
|
||||||
|
type: "workflow-error";
|
||||||
|
runId: string;
|
||||||
|
error: string;
|
||||||
|
/** Exit code conveying the failure reason (1=role error, 2=maxRounds exhausted). */
|
||||||
|
exitCode: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Workflow Worker → Parent: a WorkflowMessage produced by a role (for crash recovery). */
|
||||||
|
export type ThreadWorkflowMessage = {
|
||||||
|
type: "thread-workflow-message";
|
||||||
|
runId: string;
|
||||||
|
/** The WorkflowMessage produced by the role — persisted for crash recovery. */
|
||||||
|
message: { role: string; content: string; meta: unknown; timestamp: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Messages a workflow worker sends to the parent (subset of full worker IPC). */
|
||||||
|
export type WorkflowWorkerToParentMessage =
|
||||||
|
| ThreadEventMessage
|
||||||
|
| WorkflowErrorMessage
|
||||||
|
| ThreadWorkflowMessage;
|
||||||
|
|
||||||
|
export type WorkflowWorkerReadyMessage = { type: "ready" };
|
||||||
|
|
||||||
|
/** Messages a workflow child may emit on IPC (including bootstrap ready). */
|
||||||
|
export type WorkflowChildToParentMessage =
|
||||||
|
| WorkflowWorkerReadyMessage
|
||||||
|
| WorkflowWorkerToParentMessage;
|
||||||
|
|
||||||
|
/** Messages a workflow worker process may send upstream (ready + persisted workflow IPC). */
|
||||||
|
export type WorkflowWorkerOutboundMessage =
|
||||||
|
| WorkflowWorkerReadyMessage
|
||||||
|
| WorkflowWorkerToParentMessage;
|
||||||
|
|
||||||
|
const WORKFLOW_PARENT_MSG_TYPES = new Set([
|
||||||
|
"start-thread",
|
||||||
|
"resume-thread",
|
||||||
|
"kill-thread",
|
||||||
|
"shutdown",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function validateStartThreadMsg(obj: Record<string, unknown>): string | null {
|
||||||
|
if (typeof obj.runId !== "string") return "'start-thread' message missing string 'runId'";
|
||||||
|
if (typeof obj.workflow !== "string") return "'start-thread' message missing string 'workflow'";
|
||||||
|
if (typeof obj.prompt !== "string") return "'start-thread' message missing string 'prompt'";
|
||||||
|
if (typeof obj.maxRounds !== "number") return "'start-thread' message missing number 'maxRounds'";
|
||||||
|
if (typeof obj.dryRun !== "boolean") return "'start-thread' message missing boolean 'dryRun'";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateResumeThreadMsg(obj: Record<string, unknown>): string | null {
|
||||||
|
if (typeof obj.runId !== "string") return "'resume-thread' message missing string 'runId'";
|
||||||
|
if (!Array.isArray(obj.messages)) return "'resume-thread' message missing 'messages' array";
|
||||||
|
if (typeof obj.maxRounds !== "number")
|
||||||
|
return "'resume-thread' message missing number 'maxRounds'";
|
||||||
|
if (typeof obj.dryRun !== "boolean") return "'resume-thread' message missing boolean 'dryRun'";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseParentStartThread(obj: Record<string, unknown>): Result<StartThreadMessage> {
|
||||||
|
const errMsg = validateStartThreadMsg(obj);
|
||||||
|
if (errMsg !== null) return err(new Error(errMsg));
|
||||||
|
return ok({
|
||||||
|
type: "start-thread",
|
||||||
|
runId: obj.runId,
|
||||||
|
workflow: obj.workflow,
|
||||||
|
prompt: obj.prompt,
|
||||||
|
maxRounds: obj.maxRounds,
|
||||||
|
dryRun: obj.dryRun,
|
||||||
|
} as StartThreadMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseParentResumeThread(obj: Record<string, unknown>): Result<ResumeThreadMessage> {
|
||||||
|
const errMsg = validateResumeThreadMsg(obj);
|
||||||
|
if (errMsg !== null) return err(new Error(errMsg));
|
||||||
|
return ok({
|
||||||
|
type: "resume-thread",
|
||||||
|
runId: obj.runId,
|
||||||
|
messages: obj.messages as ResumeThreadMessage["messages"],
|
||||||
|
maxRounds: obj.maxRounds,
|
||||||
|
dryRun: obj.dryRun,
|
||||||
|
} as ResumeThreadMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseParentKillThread(obj: Record<string, unknown>): Result<KillThreadMessage> {
|
||||||
|
if (typeof obj.runId !== "string") {
|
||||||
|
return err(new Error("'kill-thread' message missing string 'runId'"));
|
||||||
|
}
|
||||||
|
return ok({ type: "kill-thread", runId: obj.runId } as KillThreadMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Validate and parse an unknown IPC message for a workflow worker process. */
|
||||||
|
export function parseWorkflowParentMessage(raw: unknown): Result<WorkflowParentToWorkerMessage> {
|
||||||
|
if (!isPlainRecord(raw)) {
|
||||||
|
return err(new Error("IPC message is not an object"));
|
||||||
|
}
|
||||||
|
const obj = raw;
|
||||||
|
if (typeof obj.type !== "string") {
|
||||||
|
return err(new Error("IPC message missing string 'type' field"));
|
||||||
|
}
|
||||||
|
if (!WORKFLOW_PARENT_MSG_TYPES.has(obj.type)) {
|
||||||
|
return err(new Error(`Unknown workflow IPC message type: "${obj.type}"`));
|
||||||
|
}
|
||||||
|
switch (obj.type) {
|
||||||
|
case "shutdown":
|
||||||
|
return ok({ type: "shutdown" });
|
||||||
|
case "start-thread":
|
||||||
|
return parseParentStartThread(obj);
|
||||||
|
case "resume-thread":
|
||||||
|
return parseParentResumeThread(obj);
|
||||||
|
case "kill-thread":
|
||||||
|
return parseParentKillThread(obj);
|
||||||
|
default:
|
||||||
|
return err(new Error(`Unhandled workflow IPC message type: "${obj.type}"`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isThreadEventType(value: string): value is ThreadEventType {
|
||||||
|
switch (value) {
|
||||||
|
case "queued":
|
||||||
|
case "started":
|
||||||
|
case "step_complete":
|
||||||
|
case "completed":
|
||||||
|
case "failed":
|
||||||
|
case "killed":
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseThreadEventMsg(obj: Record<string, unknown>): Result<ThreadEventMessage> {
|
||||||
|
if (typeof obj.runId !== "string") {
|
||||||
|
return err(new Error("Worker 'thread-event' message missing string 'runId' field"));
|
||||||
|
}
|
||||||
|
if (typeof obj.eventType !== "string" || !isThreadEventType(obj.eventType)) {
|
||||||
|
return err(
|
||||||
|
new Error(`Worker 'thread-event' message has invalid 'eventType': "${obj.eventType}"`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!("payload" in obj)) {
|
||||||
|
return err(new Error("Worker 'thread-event' message missing 'payload' field"));
|
||||||
|
}
|
||||||
|
return ok({
|
||||||
|
type: "thread-event",
|
||||||
|
runId: obj.runId,
|
||||||
|
eventType: obj.eventType,
|
||||||
|
payload: obj.payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorkflowErrorMsg(obj: Record<string, unknown>): Result<WorkflowErrorMessage> {
|
||||||
|
if (typeof obj.runId !== "string") {
|
||||||
|
return err(new Error("Worker 'workflow-error' message missing string 'runId' field"));
|
||||||
|
}
|
||||||
|
if (typeof obj.error !== "string") {
|
||||||
|
return err(new Error("Worker 'workflow-error' message missing string 'error' field"));
|
||||||
|
}
|
||||||
|
const exitCode = typeof obj.exitCode === "number" ? obj.exitCode : 1;
|
||||||
|
return ok({
|
||||||
|
type: "workflow-error",
|
||||||
|
runId: obj.runId,
|
||||||
|
error: obj.error,
|
||||||
|
exitCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseThreadWorkflowMessageMsg(obj: Record<string, unknown>): Result<ThreadWorkflowMessage> {
|
||||||
|
if (typeof obj.runId !== "string") {
|
||||||
|
return err(new Error("Worker 'thread-workflow-message' missing string 'runId' field"));
|
||||||
|
}
|
||||||
|
if (!isPlainRecord(obj.message)) {
|
||||||
|
return err(new Error("Worker 'thread-workflow-message' missing object 'message' field"));
|
||||||
|
}
|
||||||
|
const msg = obj.message;
|
||||||
|
if (typeof msg.role !== "string") {
|
||||||
|
return err(new Error("Worker 'thread-workflow-message' message missing string 'role' field"));
|
||||||
|
}
|
||||||
|
if (typeof msg.content !== "string") {
|
||||||
|
return err(
|
||||||
|
new Error("Worker 'thread-workflow-message' message missing string 'content' field"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof msg.timestamp !== "number") {
|
||||||
|
return err(
|
||||||
|
new Error("Worker 'thread-workflow-message' message missing number 'timestamp' field"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ok({
|
||||||
|
type: "thread-workflow-message",
|
||||||
|
runId: obj.runId,
|
||||||
|
message: {
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content,
|
||||||
|
meta: "meta" in msg ? msg.meta : null,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKFLOW_WORKER_MSG_TYPES = new Set([
|
||||||
|
"thread-event",
|
||||||
|
"workflow-error",
|
||||||
|
"thread-workflow-message",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Validate and parse workflow worker → parent IPC messages. */
|
||||||
|
export function parseWorkflowWorkerToParentMessage(
|
||||||
|
raw: unknown,
|
||||||
|
): Result<WorkflowWorkerToParentMessage> {
|
||||||
|
if (!isPlainRecord(raw)) {
|
||||||
|
return err(new Error("Worker IPC message is not an object"));
|
||||||
|
}
|
||||||
|
const obj = raw;
|
||||||
|
if (typeof obj.type !== "string") {
|
||||||
|
return err(new Error("Worker IPC message missing string 'type' field"));
|
||||||
|
}
|
||||||
|
if (!WORKFLOW_WORKER_MSG_TYPES.has(obj.type)) {
|
||||||
|
return err(new Error(`Unknown workflow worker IPC message type: "${obj.type}"`));
|
||||||
|
}
|
||||||
|
if (obj.type === "thread-event") return parseThreadEventMsg(obj);
|
||||||
|
if (obj.type === "workflow-error") return parseWorkflowErrorMsg(obj);
|
||||||
|
return parseThreadWorkflowMessageMsg(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse messages from a workflow worker child (thread IPC + optional `ready`). */
|
||||||
|
export function parseWorkflowChildMessage(
|
||||||
|
raw: unknown,
|
||||||
|
): Result<WorkflowChildToParentMessage> {
|
||||||
|
if (!isPlainRecord(raw)) {
|
||||||
|
return err(new Error("Worker IPC message is not an object"));
|
||||||
|
}
|
||||||
|
const obj = raw;
|
||||||
|
if (typeof obj.type !== "string") {
|
||||||
|
return err(new Error("Worker IPC message missing string 'type' field"));
|
||||||
|
}
|
||||||
|
if (obj.type === "ready") {
|
||||||
|
return ok({ type: "ready" });
|
||||||
|
}
|
||||||
|
return parseWorkflowWorkerToParentMessage(raw);
|
||||||
|
}
|
||||||
+10
-19
@@ -2,15 +2,13 @@
|
|||||||
* Pure helpers and IPC branching for workflow-manager (keeps workflow-manager.ts lean).
|
* Pure helpers and IPC branching for workflow-manager (keeps workflow-manager.ts lean).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { dirname, join } from "node:path";
|
import { isPlainRecord } from "@uncaged/nerve-core";
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
import type { WorkflowMessage } from "@uncaged/nerve-core";
|
|
||||||
import { START, isPlainRecord } from "@uncaged/nerve-core";
|
|
||||||
|
|
||||||
import type { LogStore, WorkflowRunStatus } from "@uncaged/nerve-store";
|
import type { LogStore, WorkflowRunStatus } from "@uncaged/nerve-store";
|
||||||
import type { ResumeThreadMessage, ThreadEventMessage } from "./ipc.js";
|
import type { ResumeThreadMessage, ThreadEventMessage } from "./ipc.js";
|
||||||
import type { WorkerToParentMessage } from "./ipc.js";
|
import type { WorkflowChildToParentMessage } from "./ipc.js";
|
||||||
|
import type { WorkflowMessage } from "./types.js";
|
||||||
|
import { START } from "./types.js";
|
||||||
|
|
||||||
export type PendingThread = {
|
export type PendingThread = {
|
||||||
runId: string;
|
runId: string;
|
||||||
@@ -33,7 +31,7 @@ export const WORKFLOW_WORKER_RESPAWN = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Worker shutdown timeout — must stay in sync with SHUTDOWN_TIMEOUT_MS in workflow-worker.ts.
|
* Worker shutdown timeout — must stay in sync with shutdown handling in `worker.ts`.
|
||||||
* The drain timeout passed to drainAndRespawn must be >= this value so the worker has
|
* The drain timeout passed to drainAndRespawn must be >= this value so the worker has
|
||||||
* enough time to finish in-flight threads before the parent force-kills it.
|
* enough time to finish in-flight threads before the parent force-kills it.
|
||||||
*/
|
*/
|
||||||
@@ -79,12 +77,6 @@ export function ensureThreadMessagesWithStart(
|
|||||||
return [start, ...mapped];
|
return [start, ...mapped];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveWorkflowWorkerScript(): string {
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dir = dirname(__filename);
|
|
||||||
return join(__dir, "workflow-worker.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapWorkflowRunStatus(eventType: string): WorkflowRunStatus | null {
|
export function mapWorkflowRunStatus(eventType: string): WorkflowRunStatus | null {
|
||||||
const map: Record<string, WorkflowRunStatus> = {
|
const map: Record<string, WorkflowRunStatus> = {
|
||||||
started: "started",
|
started: "started",
|
||||||
@@ -223,9 +215,13 @@ export type WorkflowManagerMessageDeps = {
|
|||||||
|
|
||||||
export function dispatchWorkflowWorkerMessage(
|
export function dispatchWorkflowWorkerMessage(
|
||||||
workflowName: string,
|
workflowName: string,
|
||||||
msg: WorkerToParentMessage,
|
msg: WorkflowChildToParentMessage,
|
||||||
deps: WorkflowManagerMessageDeps,
|
deps: WorkflowManagerMessageDeps,
|
||||||
): void {
|
): void {
|
||||||
|
if (msg.type === "ready") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "thread-event") {
|
if (msg.type === "thread-event") {
|
||||||
deps.handleThreadEvent(workflowName, msg);
|
deps.handleThreadEvent(workflowName, msg);
|
||||||
return;
|
return;
|
||||||
@@ -247,10 +243,5 @@ export function dispatchWorkflowWorkerMessage(
|
|||||||
`[workflow-manager] workflow-error for runId "${msg.runId}" in "${workflowName}": ${msg.error}\n`,
|
`[workflow-manager] workflow-error for runId "${msg.runId}" in "${workflowName}": ${msg.error}\n`,
|
||||||
);
|
);
|
||||||
deps.onWorkflowRoleError(workflowName, msg.runId, msg.error, msg.exitCode);
|
deps.onWorkflowRoleError(workflowName, msg.runId, msg.error, msg.exitCode);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.type === "error") {
|
|
||||||
process.stderr.write(`[workflow-manager] error from "${workflowName}" worker: ${msg.error}\n`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,12 +10,13 @@ import type { NerveConfig, WorkflowConfig, WorkflowStatus } from "@uncaged/nerve
|
|||||||
|
|
||||||
import type { LogStore } from "@uncaged/nerve-store";
|
import type { LogStore } from "@uncaged/nerve-store";
|
||||||
import type { KillThreadMessage, StartThreadMessage, ThreadEventMessage } from "./ipc.js";
|
import type { KillThreadMessage, StartThreadMessage, ThreadEventMessage } from "./ipc.js";
|
||||||
import { parseWorkerMessage } from "./ipc.js";
|
import { parseWorkflowChildMessage } from "./ipc.js";
|
||||||
import {
|
import {
|
||||||
createWorkerRuntime,
|
createWorkerRuntime,
|
||||||
formatCapturedStderrTail,
|
formatCapturedStderrTail,
|
||||||
formatChildExitSummary,
|
formatChildExitSummary,
|
||||||
} from "./worker-runtime.js";
|
} from "./worker-runtime.js";
|
||||||
|
import { WORKFLOW_WORKER_PATH } from "./paths.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_MAX_QUEUE,
|
DEFAULT_MAX_QUEUE,
|
||||||
WORKER_SHUTDOWN_TIMEOUT_MS,
|
WORKER_SHUTDOWN_TIMEOUT_MS,
|
||||||
@@ -25,8 +26,7 @@ import {
|
|||||||
dispatchWorkflowWorkerMessage,
|
dispatchWorkflowWorkerMessage,
|
||||||
extractExitCodeFromPayload,
|
extractExitCodeFromPayload,
|
||||||
recoverThreadsFromStore,
|
recoverThreadsFromStore,
|
||||||
resolveWorkflowWorkerScript,
|
} from "./manager-support.js";
|
||||||
} from "./workflow-manager-support.js";
|
|
||||||
|
|
||||||
export type WorkflowLaunchParams = {
|
export type WorkflowLaunchParams = {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
@@ -73,7 +73,7 @@ export function createWorkflowManager(
|
|||||||
initialConfig: NerveConfig,
|
initialConfig: NerveConfig,
|
||||||
logStore: LogStore,
|
logStore: LogStore,
|
||||||
): WorkflowManager {
|
): WorkflowManager {
|
||||||
const workerScript = resolveWorkflowWorkerScript();
|
const workerScript = WORKFLOW_WORKER_PATH;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default drain timeout must be at least WORKER_SHUTDOWN_TIMEOUT_MS so the worker
|
* Default drain timeout must be at least WORKER_SHUTDOWN_TIMEOUT_MS so the worker
|
||||||
@@ -309,7 +309,7 @@ export function createWorkflowManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleWorkerMessage(workflowName: string, raw: unknown): void {
|
function handleWorkerMessage(workflowName: string, raw: unknown): void {
|
||||||
const result = parseWorkerMessage(raw);
|
const result = parseWorkflowChildMessage(raw);
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`[workflow-manager] invalid message from "${workflowName}" worker: ${result.error.message}\n`,
|
`[workflow-manager] invalid message from "${workflowName}" worker: ${result.error.message}\n`,
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
/** Resolved path to the workflow child-process entry (`worker.js` adjacent to this module in dist). */
|
||||||
|
export const WORKFLOW_WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), "worker.js");
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Narrow surface for `@uncaged/nerve-core` — workflow automaton types + config only.
|
||||||
|
* Keeps core's declaration graph free of IPC / manager / store.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { START, END, DEFAULT_ENGINE_MAX_ROUNDS } from "./types.js";
|
||||||
|
export type {
|
||||||
|
WorkflowMessage,
|
||||||
|
RoleResult,
|
||||||
|
Role,
|
||||||
|
RoleMeta,
|
||||||
|
StartStep,
|
||||||
|
ThreadContext,
|
||||||
|
WorkflowContext,
|
||||||
|
AgentFn,
|
||||||
|
RoleStep,
|
||||||
|
ModeratorContext,
|
||||||
|
Moderator,
|
||||||
|
WorkflowDefinition,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
export type { DropOverflowConfig, QueueOverflowConfig, WorkflowConfig } from "./config.js";
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Worker-process signal handling (fork IPC children only).
|
||||||
|
* Worker entrypoints import this module — not worker-runtime.ts (parent/kernel code).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forked workers inherit the parent's process group. In foreground `nerve dev`,
|
||||||
|
* terminal-driven SIGINT/SIGTERM is delivered to the whole group, so workers can exit
|
||||||
|
* on the default handler before the kernel sends `{ type: "shutdown" }` over IPC.
|
||||||
|
* Swallow these in worker processes so the parent coordinates shutdown (issue #55).
|
||||||
|
* Only call when `process.send` is defined (fork IPC); standalone `node …-worker.js` keeps default Ctrl+C behaviour.
|
||||||
|
*/
|
||||||
|
export function ignoreSessionBroadcastSignals(): void {
|
||||||
|
const swallow = (): void => {};
|
||||||
|
process.on("SIGINT", swallow);
|
||||||
|
process.on("SIGTERM", swallow);
|
||||||
|
}
|
||||||
@@ -14,6 +14,14 @@ import "./experimental-warning-suppression.js";
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { join, resolve } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
|
|
||||||
|
import { isPlainRecord } from "@uncaged/nerve-core";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ThreadEventType,
|
||||||
|
ThreadWorkflowMessage,
|
||||||
|
WorkflowWorkerOutboundMessage,
|
||||||
|
} from "./ipc.js";
|
||||||
|
import { parseWorkflowParentMessage } from "./ipc.js";
|
||||||
import type {
|
import type {
|
||||||
RoleMeta,
|
RoleMeta,
|
||||||
RoleStep,
|
RoleStep,
|
||||||
@@ -21,22 +29,15 @@ import type {
|
|||||||
ThreadContext,
|
ThreadContext,
|
||||||
WorkflowDefinition,
|
WorkflowDefinition,
|
||||||
WorkflowMessage,
|
WorkflowMessage,
|
||||||
} from "@uncaged/nerve-core";
|
} from "./types.js";
|
||||||
import { END, START, isPlainRecord } from "@uncaged/nerve-core";
|
import { END, START } from "./types.js";
|
||||||
|
|
||||||
import type {
|
|
||||||
ThreadEventType,
|
|
||||||
ThreadWorkflowMessageMessage,
|
|
||||||
WorkerToParentMessage,
|
|
||||||
} from "./ipc.js";
|
|
||||||
import { parseParentMessage } from "./ipc.js";
|
|
||||||
import { ignoreSessionBroadcastSignals } from "./worker-signals.js";
|
import { ignoreSessionBroadcastSignals } from "./worker-signals.js";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// IPC helpers
|
// IPC helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function send(msg: WorkerToParentMessage): void {
|
function send(msg: WorkflowWorkerOutboundMessage): void {
|
||||||
if (process.send) {
|
if (process.send) {
|
||||||
process.send(msg);
|
process.send(msg);
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ function sendWorkflowError(runId: string, error: string, exitCode = 1): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendWorkflowMessage(runId: string, message: WorkflowMessage): void {
|
function sendWorkflowMessage(runId: string, message: WorkflowMessage): void {
|
||||||
const msg: ThreadWorkflowMessageMessage = {
|
const msg: ThreadWorkflowMessage = {
|
||||||
type: "thread-workflow-message",
|
type: "thread-workflow-message",
|
||||||
runId,
|
runId,
|
||||||
message: {
|
message: {
|
||||||
@@ -334,7 +335,7 @@ function handleMessage(
|
|||||||
killFlags: Map<string, KillFlag>,
|
killFlags: Map<string, KillFlag>,
|
||||||
shuttingDown: { value: boolean },
|
shuttingDown: { value: boolean },
|
||||||
): void {
|
): void {
|
||||||
const parseResult = parseParentMessage(raw);
|
const parseResult = parseWorkflowParentMessage(raw);
|
||||||
if (!parseResult.ok) {
|
if (!parseResult.ok) {
|
||||||
process.stderr.write(`[workflow-worker] Invalid IPC message: ${parseResult.error.message}\n`);
|
process.stderr.write(`[workflow-worker] Invalid IPC message: ${parseResult.error.message}\n`);
|
||||||
return;
|
return;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"composite": false,
|
||||||
|
"noEmit": false
|
||||||
|
},
|
||||||
|
"include": ["src/types.ts", "src/config.ts", "src/public-types.ts"]
|
||||||
|
}
|
||||||
Generated
+13
@@ -80,6 +80,9 @@ importers:
|
|||||||
'@uncaged/nerve-daemon':
|
'@uncaged/nerve-daemon':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../daemon
|
version: link:../daemon
|
||||||
|
'@uncaged/workflow':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../workflow
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.1.5
|
specifier: ^4.1.5
|
||||||
version: 4.1.5(@types/node@22.19.17)(vite@8.0.9(@types/node@22.19.17)(esbuild@0.27.7)(yaml@2.8.3))
|
version: 4.1.5(@types/node@22.19.17)(vite@8.0.9(@types/node@22.19.17)(esbuild@0.27.7)(yaml@2.8.3))
|
||||||
@@ -111,6 +114,9 @@ importers:
|
|||||||
'@uncaged/nerve-store':
|
'@uncaged/nerve-store':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../store
|
version: link:../store
|
||||||
|
'@uncaged/workflow':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../workflow
|
||||||
yaml:
|
yaml:
|
||||||
specifier: ^2.8.3
|
specifier: ^2.8.3
|
||||||
version: 2.8.3
|
version: 2.8.3
|
||||||
@@ -219,6 +225,13 @@ importers:
|
|||||||
version: 4.1.5(@types/node@22.19.17)(vite@8.0.9(@types/node@22.19.17)(esbuild@0.27.7)(yaml@2.8.3))
|
version: 4.1.5(@types/node@22.19.17)(vite@8.0.9(@types/node@22.19.17)(esbuild@0.27.7)(yaml@2.8.3))
|
||||||
|
|
||||||
packages/workflow:
|
packages/workflow:
|
||||||
|
dependencies:
|
||||||
|
'@uncaged/nerve-core':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core
|
||||||
|
'@uncaged/nerve-store':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../store
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@rslib/core':
|
'@rslib/core':
|
||||||
specifier: ^0.21.3
|
specifier: ^0.21.3
|
||||||
|
|||||||
Reference in New Issue
Block a user