docs: rebalance prose cleanup and add trimming skill
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Host side of one workflow run. Owns the worker, child RPC, first-outcome
|
||||
* settlement, cancellation grace, lifecycle pairing, and quiescent cleanup.
|
||||
* Host side of one workflow run. The first worker result, unexpected death, or
|
||||
* cancellation-grace expiry owns settlement and closes message admission.
|
||||
* Pending starts share one abort signal; published children share idempotent
|
||||
* cleanup, and quiescence waits for both while synthesizing any missing end events.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/host
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
/**
|
||||
* The `node:worker_threads` workflow engine: the {@link WorkflowService} implementation.
|
||||
* Worker-thread workflow engine. Each run executes its model-written script in
|
||||
* an escapable vm context on a fresh worker and bridges `agent()` calls to host
|
||||
* subagents. The thread prevents synchronous script work from blocking the host
|
||||
* and permits forced termination, but it is containment rather than a security boundary.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Meta validation: check the caller-provided {@link WorkflowMeta} DATA against the shape
|
||||
* contract and reject everything else loud, every violation named.
|
||||
* contract and reject everything else loud, every violation named. Meta arrives as schema-checked
|
||||
* JSON data, never evaluated script text; evaluating it on the host could run getters outside the
|
||||
* worker timeout that exists to isolate model-written code.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/meta
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* The host⇄worker wire protocol: one string-valued enum of message tags per direction, a
|
||||
* payload map giving each tag its parameters (the single source of truth), and the message
|
||||
* unions derived from them.
|
||||
* unions derived from them. Payloads are plain JSON by construction for structured clone. Both
|
||||
* directions are closed engine protocols whose receivers use `assertNever`; generic typed senders
|
||||
* make tag/payload mismatches compile-time errors rather than silently skipped messages.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/protocol
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/**
|
||||
* The engine's value boundary: copy script-realm values into plain JSON data — loud about
|
||||
* everything JSON cannot carry — and render thrown script values to failure text.
|
||||
* Materializes values leaving the script vm into plain JSON before they cross the worker
|
||||
* boundary, and renders thrown script values without rejecting the run. The walk rejects
|
||||
* lossy JSON shapes but trusts model-written workflow scripts: getters and proxy traps may
|
||||
* run, and the vm is not a security boundary. The worker provides host-loop isolation and
|
||||
* forced termination, not hostile-value containment. See
|
||||
* docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md for the isolation rationale.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/realm
|
||||
*/
|
||||
|
||||
@@ -48,11 +52,16 @@ function hasPlainPrototype(value: object): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy `value` (typically from the vm realm) into plain host JSON data.
|
||||
* Copy `value` (typically from the vm realm) into plain host JSON data. Root `undefined` is
|
||||
* returned unchanged; nested `undefined` and values JSON cannot represent losslessly fail
|
||||
* with the offending path. Property accessors run normally, and a throwing read is wrapped
|
||||
* with its rendered failure.
|
||||
*
|
||||
* @param value - the realm value to materialize.
|
||||
* @param root - the path label for the root value (error messages).
|
||||
* @returns the host-realm copy (plain objects/arrays/scalars only).
|
||||
* @throws {@link MaterializeError} for unsupported values, cycles, sparse arrays, exotic
|
||||
* prototypes, or property reads that throw.
|
||||
*/
|
||||
export function materializeFromRealm(value: unknown, root = 'value'): unknown {
|
||||
if (value === undefined) return undefined
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
/**
|
||||
* Worker-side workflow runtime: vm hooks, child RPC, limits, value
|
||||
* materialization, cancellation, and result shaping. Host termination enforces
|
||||
* the cancellation deadline.
|
||||
* Per-run worker-side vm hooks, child RPC, concurrency/caps, cancellation, and result shaping; it
|
||||
* never touches Cordis. Script values leaving the realm are materialized as plain JSON before
|
||||
* messaging. Values entering the trusted model-written realm are passed directly; `args` alone is
|
||||
* cloned so script mutation cannot alter initialization data. See `./realm.ts` for the trust model.
|
||||
*
|
||||
* Fatal workflow errors—bad hook arguments, unsupported schemas/options, caps, start failures, and
|
||||
* cancellation—propagate through combinators. Only child failures and ordinary stage errors become
|
||||
* per-item nulls. Every returned promise has a rejection consumer so dropped script promises cannot
|
||||
* kill the worker. A cancelled script that never settles emits nothing; the host force-settles the
|
||||
* run within grace and terminates the thread.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/runtime
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/**
|
||||
* The worker-side half of the engine: {@link runWorkerSession} wires one MessagePort to one
|
||||
* {@link WorkflowExecution} — hook progress and child starts go out as messages, run control
|
||||
* and child lifecycle come back in — and posts the run's terminal result exactly once.
|
||||
* and child lifecycle come back in — and posts the run's terminal result exactly once. Keeping it
|
||||
* separate from `worker.ts` lets unit tests drive the session over a MessageChannel, because main
|
||||
* process coverage cannot observe code inside a real Worker.
|
||||
*
|
||||
* The session announces ready and waits for `go`, so cancellation racing startup can prevent even
|
||||
* the script's synchronous prefix. A cancel in place of `go` releases the gate into a cancelled
|
||||
* drive without executing the body.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/session
|
||||
*/
|
||||
|
||||
@@ -127,8 +133,9 @@ export function requireParentPort(port: MessagePort | null): MessagePort {
|
||||
/**
|
||||
* Run one workflow script to settlement against `port`, posting the terminal result message
|
||||
* exactly once; resolves after that post (stray children may still be winding down through the
|
||||
* port — the host owns their teardown and ultimately terminates the thread).
|
||||
*
|
||||
* port — the host owns their teardown and ultimately terminates the thread). It never rejects:
|
||||
* constructor failure becomes an error result. Host pre-parse makes syntax failure here a likely
|
||||
* Node-version skew, but the session still reports it instead of dying silently.
|
||||
* @param port - the channel to the host (the real `parentPort`, or one side
|
||||
* of an in-process `MessageChannel` in tests).
|
||||
* @param init - the run payload the host provided as `workerData`.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Non-protocol wire vocabulary for the worker-thread engine: the `workerData` init payload and
|
||||
* the child-port interfaces the worker-side runtime consumes.
|
||||
* the child-port interfaces the worker-side runtime consumes. Host/worker messages are defined in
|
||||
* `./protocol.ts`; transported child requests and results are plain JSON for structured clone.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/types
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* The worker-thread entry the engine spawns: bootstrap ./session.ts on the real `parentPort`.
|
||||
* Single-statement worker entry that boots `runWorkerSession` on real `parentPort`. Logic remains in
|
||||
* the session module for in-process MessageChannel coverage; importing this entry on the main thread
|
||||
* exercises `requireParentPort`'s failure path.
|
||||
* @module @deepseek-ai/dsh-workflow-workerthread/worker
|
||||
*/
|
||||
|
||||
|
||||
@@ -437,10 +437,7 @@ describe('runWorkerSession over an in-process MessageChannel', () => {
|
||||
void runWorkerSession(host.port, init("return await agent('p')"))
|
||||
await vi.waitFor(() => { expect(host.ofType(WorkerToHostType.ChildStart).length).toBe(1) })
|
||||
const callId = host.ofType(WorkerToHostType.ChildStart)[0]!.callId
|
||||
// Cancel FIRST, then the (stale) started reply: the worker processes them
|
||||
// in order, so the agent() continuation resumes already-cancelled — the
|
||||
// window the real host cannot produce (it refuses starts once cancelled)
|
||||
// but a teardown race can.
|
||||
// Simulate a teardown race by delivering cancellation before a stale start reply.
|
||||
host.send({ type: HostToWorkerType.Cancel, reason: 'raced the start' })
|
||||
host.send({ type: HostToWorkerType.ChildStarted, callId, childId: 'child-0' })
|
||||
const result = await host.result()
|
||||
@@ -448,7 +445,7 @@ describe('runWorkerSession over an in-process MessageChannel', () => {
|
||||
await vi.waitFor(() => {
|
||||
expect(host.ofType(WorkerToHostType.ChildDispose).map(m => m.callId)).toContain(callId)
|
||||
})
|
||||
// The child never became an agent-start: it was wound down pre-lifecycle.
|
||||
// The unpublished child is disposed without a lifecycle announcement.
|
||||
expect(host.ofType(WorkerToHostType.AgentStart)).toEqual([])
|
||||
host.close()
|
||||
})
|
||||
|
||||
@@ -19,7 +19,11 @@ function fakeParent(): Agent {
|
||||
// Allow cold worker startup on contended CI runners.
|
||||
vi.setConfig({ testTimeout: 30_000 })
|
||||
|
||||
/** Retry an assertion until it passes or the timeout elapses. */
|
||||
/**
|
||||
* Wait up to 10 seconds for CPU-bound worker startup or cross-thread delivery on contended CI.
|
||||
* Host reactions after an observed event use explicit tight overrides, so this generous startup
|
||||
* allowance cannot hide multi-second reap regressions.
|
||||
*/
|
||||
function waitFor(assertion: () => void, timeout = 10_000): Promise<void> {
|
||||
return vi.waitFor(assertion, { timeout, interval: 50 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user