workflow: dynamic workflows — script-driven multi-agent orchestration

A new capability family at packages/workflow/ in the bash seam shape,
modeled on Claude Code's dynamic workflows: the model writes a JavaScript
orchestration script (export const meta = {...} + plain-JS body), a runtime
executes it, and the script — not the conversation — holds the loop, the
branching, and the intermediate results.

- dsh-workflow (ctx.workflows): abstract WorkflowService + run vocabulary
  (WorkflowRun whose result NEVER rejects) + observe-only workflow/* events
  carrying data snapshots (id + meta, never the live run), per-listener
  contained like subagent/*.
- dsh-workflow-vm: in-process node:vm engine. Meta extraction via a
  string/comment-aware scanner (template interpolation rejected; literal
  evaluated alone in an empty timed context; statement blanked line-
  preservingly so stacks keep script line numbers). Hooks: agent(prompt,
  {label, phase, schema, model}) over ctx.subagents, parallel(), pipeline()
  (no cross-stage barrier), phase(), log(), args. Fatal-vs-null discipline:
  hook misuse (unknown/deferred options, bad arguments, unsupported
  schemas, tripped caps, seam start failures, cancellation) throws fatal
  WorkflowErrors the combinators RE-THROW — never dissolved into the
  per-item null reserved for child failures. Realm boundary: inbound values
  materialized by descriptor walks that never invoke accessors (defineProperty
  copies, __proto__-safe); outbound values rebuilt in-realm via the
  context's own JSON.parse. Determinism bans (Date.now/Math.random/argless
  new Date) kept so future resume support cannot break scripts. Caps and
  timeouts are validated Config. Every hook promise carries a no-op
  rejection consumer (app-boot exits on unhandled rejections).
- dsh-tool-workflow: the model-facing workflow tool, synchronous like
  dsh-tool-subagent (start → await → try/finally dispose; abort bridged;
  non-completed → isError). Generic render card titled by a textual
  meta.name sniff. The tool description carries the authoring contract.

Wired into examples/{coding-agent,acp-agent} with explicit-ask-only
guidance. Coverage at every tier: unit (meta scanner, materializer incl.
counting-getter and __proto__ regressions, combinator semantics,
concurrency ceiling, caps, cancellation, no-unhandled-rejection abandon),
integration over the real spawn stack, with-key e2e (real two-phase run +
the tool through the registry pipeline), and a recorded ACP snapshot
scenario (workflow-run, 1 child session). RFC:
docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md (deferred
work explicitly listed). AGENTS.md budget 1575 → 1590 for the new group's
layout line.
This commit is contained in:
Tianyi Cui
2026-07-05 13:29:35 +08:00
parent dafb81be7b
commit 1d43ea3cd5
52 changed files with 4459 additions and 109 deletions

View File

@@ -0,0 +1,29 @@
# @deepseek-ai/dsh-workflow
The **workflow seam** (`ctx.workflows`): an abstract service defining WHAT a workflow engine does — execute a model-written orchestration script that fans out subagents — without saying HOW. The bash-shaped third of the [workflow family](../README.md): implementations subclass `WorkflowService` and register as the `workflows` service (one per context); [`dsh-workflow-vm`](../workflow-vm/README.md) is the first, and [`dsh-tool-workflow`](../tool-workflow/README.md) is the model-facing consumer.
## Service: `WorkflowService` (abstract)
`start(request: WorkflowStartRequest): WorkflowRun` — parse and execute a script. Throws synchronously (`SCRIPT_PARSE`/`META_INVALID`) for a script that cannot begin; once a run is returned, its `result` NEVER rejects — every failure resolves with `stopReason: 'error'` (or `'cancelled'`). `dispose()` must reach quiescence within a bounded grace (cancel → wait → abandon), never hanging its caller.
The protected `emitWorkflowEvent` helper dispatches the `workflow/*` events with PER-LISTENER containment (a throwing subscriber is logged, never propagated, and cannot starve later listeners) — the same guarantee as the subagent seam's lifecycle emits.
## Vocabulary
- `WorkflowStartRequest``{ script, args?, parent: Agent, signal? }`. `parent` is REQUIRED: every child the script spawns is attributed to it. `args` must be plain host-realm JSON data.
- `WorkflowMeta` / `WorkflowPhase` — the script's validated `export const meta` block (Claude Code format: required `name`/`description`, optional `whenToUse`/`phases`).
- `WorkflowRun``{ id, meta, result, cancel(reason?), dispose() }`; the consumer awaits `result` and MUST `dispose` on every path.
- `WorkflowResult``{ value, stopReason: 'completed'|'cancelled'|'error', error?, agentsStarted }`; `value` is the script's materialized return (plain JSON data; `null` for no return).
- `WorkflowError``HarnessError` with a `WorkflowErrorCode` and a `fatal` flag driving the combinator discipline: a fatal error (bad hook arguments, unsupported options/schemas, tripped caps, seam start failures, cancellation) always propagates through `parallel()`/`pipeline()` instead of dissolving into a per-item `null`. `isFatalWorkflowError(error)` is the catch-site predicate.
## Events
All observe-only emits carrying DATA SNAPSHOTS (`WorkflowRunInfo` = id + meta) — never the live `WorkflowRun`, so a listener cannot gain `cancel`/`dispose`; control stays with the `start()` caller:
- `workflow/start`(info) / `workflow/end`(info, resultInfo) — run lifecycle; `resultInfo` deliberately omits the value.
- `workflow/phase`(info, title) / `workflow/log`(info, message) — script narration.
- `workflow/agent-start`(info, agent) / `workflow/agent-end`(info, agent + outcome) — one pair per `agent()` call, correlated by `seq`.
## Non-goals (this cut)
Background collection, journaling/resume, saved workflows, nested `workflow()`, token budgets — see the [RFC's deferred section](../../../docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md).

View File

@@ -0,0 +1,37 @@
{
"name": "@deepseek-ai/dsh-workflow",
"description": "Workflow capability seam: ctx.workflows service, run vocabulary, and workflow/* events",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-brand": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"cordis": "^4.0.0-rc.6"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"cordis": "^4.0.0-rc.6"
}
}

View File

@@ -0,0 +1,224 @@
/**
* The workflow capability seam (`ctx.workflows`): an abstract service defining
* WHAT a workflow engine does — execute a model-written orchestration script
* that fans out subagents — without saying HOW. Implementations subclass
* {@link WorkflowService} and register as the `workflows` service (one
* implementation per context, cordis' standard duplicate-service behavior);
* `@deepseek-ai/dsh-workflow-vm` (an in-process `node:vm` engine) is the
* first. Future engines (a worker-thread or isolated-vm sandbox) swap in
* without touching the model-facing tool that consumes them
* (`@deepseek-ai/dsh-tool-workflow`).
*
* The `workflow/*` lifecycle events are OBSERVE-ONLY data snapshots: they
* carry {@link WorkflowRunInfo} (id + meta), never the live {@link WorkflowRun}
* — a listener must not gain `cancel`/`dispose`; control stays with the
* `start()` caller holding the run. Every emit is per-listener contained (a
* throwing subscriber is logged, never propagated), so one bad observer can
* neither strand a live run nor starve later listeners.
*
* @module @deepseek-ai/dsh-workflow
*/
import { Context, Service } from 'cordis'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import type {
WorkflowAgentEndInfo,
WorkflowAgentInfo,
WorkflowResultInfo,
WorkflowRun,
WorkflowRunInfo,
WorkflowStartRequest,
} from './types.ts'
export { WorkflowRunId } from './types.ts'
export type {
WorkflowAgentEndInfo,
WorkflowAgentInfo,
WorkflowAgentOutcome,
WorkflowMeta,
WorkflowPhase,
WorkflowResult,
WorkflowResultInfo,
WorkflowRun,
WorkflowRunInfo,
WorkflowStartRequest,
WorkflowStopReason,
} from './types.ts'
declare module 'cordis' {
interface Context {
workflows: WorkflowService
}
interface Events {
/**
* A workflow run started — the script's meta block validated, the body
* about to execute. Paired with {@link Events['workflow/end']}.
* @param info - the run's identity snapshot (id + meta).
* @mode emit
*/
'workflow/start'(info: WorkflowRunInfo): void
/**
* The script entered a phase (a `phase(title)` call) — progress grouping
* for observers; no execution semantics.
* @param info - the run's identity snapshot.
* @param title - the phase title, verbatim.
* @mode emit
*/
'workflow/phase'(info: WorkflowRunInfo, title: string): void
/**
* The script emitted a narration line (a `log(message)` call).
* @param info - the run's identity snapshot.
* @param message - the logged message, verbatim.
* @mode emit
*/
'workflow/log'(info: WorkflowRunInfo, message: string): void
/**
* One `agent()` call started a child run. Paired with
* {@link Events['workflow/agent-end']} by `agent.seq`.
* @param info - the run's identity snapshot.
* @param agent - the call's sequence number, label, phase, and child id.
* @mode emit
*/
'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void
/**
* One `agent()` call settled (clean result, child failure, or run
* cancellation). Paired with {@link Events['workflow/agent-start']}.
* @param info - the run's identity snapshot.
* @param agent - the call identity plus its outcome.
* @mode emit
*/
'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void
/**
* A workflow run settled (any stop reason). Fired when
* {@link WorkflowRun.result} resolves. Paired with
* {@link Events['workflow/start']}.
* @param info - the run's identity snapshot.
* @param result - the outcome data (stop reason, error, agent count) —
* deliberately WITHOUT the result value (see {@link WorkflowResultInfo}).
* @mode emit
*/
'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void
}
}
/** The full set of `workflow/*` event names {@link WorkflowService.emitWorkflowEvent} dispatches. */
export type WorkflowEventName =
| 'workflow/start'
| 'workflow/phase'
| 'workflow/log'
| 'workflow/agent-start'
| 'workflow/agent-end'
| 'workflow/end'
/**
* The workflow-seam error codes. Every one of these is FATAL when it reaches
* a script (see {@link WorkflowError.fatal}): the combinators re-throw it
* instead of dissolving it into an ordinary per-item `null`.
*
* - `SCRIPT_PARSE` — the script (or its meta statement) does not parse.
* - `META_INVALID` — the meta block evaluated but fails the shape contract.
* - `INVALID_ARGUMENT` — a hook was called with malformed arguments.
* - `UNSUPPORTED_OPTION` — an `agent()` option this engine does not support
* (deferred: `effort`/`isolation`/`agentType`) or does not know.
* - `UNSUPPORTED_SCHEMA` — an `agent()` schema outside the structured-output
* subset (see dsh-tools).
* - `AGENT_CAP` / `ITEM_CAP` — the run/agent caps tripped.
* - `AGENT_START` — the subagent seam refused to start a child.
* - `RESULT_UNSERIALIZABLE` — a value crossing the realm boundary is not
* plain JSON data.
* - `CANCELLED` — the run was cancelled; pending and future hooks reject
* with this (the script-kill mechanism).
*/
export type WorkflowErrorCode =
| 'SCRIPT_PARSE'
| 'META_INVALID'
| 'INVALID_ARGUMENT'
| 'UNSUPPORTED_OPTION'
| 'UNSUPPORTED_SCHEMA'
| 'AGENT_CAP'
| 'ITEM_CAP'
| 'AGENT_START'
| 'RESULT_UNSERIALIZABLE'
| 'CANCELLED'
/**
* Typed error for workflow-seam failures. Extends {@link HarnessError}, so the
* `code` is machine-routable taxonomy. `fatal` drives the combinator
* discipline: `parallel()`/`pipeline()` re-throw a fatal error (a typo'd
* option or a tripped cap must kill the script loudly), and reserve the
* per-item `null` for child-run failures and ordinary in-stage script errors.
* Every {@link WorkflowErrorCode} is fatal in this cut; the flag exists so the
* distinction is explicit at every catch site rather than implied.
*/
export class WorkflowError extends HarnessError {
/** Whether combinators must propagate this error instead of nulling the item. */
readonly fatal: boolean
constructor(message: string, code: WorkflowErrorCode, options?: ErrorOptions & { fatal?: boolean }) {
super(message, code, options)
this.name = 'WorkflowError'
this.fatal = options?.fatal ?? true
}
}
/** Whether combinators must re-throw `error` instead of mapping the item to `null`. */
export function isFatalWorkflowError(error: unknown): boolean {
return error instanceof WorkflowError && error.fatal
}
/**
* Abstract workflow execution service. Subclass, implement {@link start}, and
* load the subclass as a plugin — it registers as `ctx.workflows` (one
* implementation per context; loading a second throws, cordis' standard
* duplicate-service behavior).
*
* Semantics every implementation must honor:
* - {@link start} throws synchronously for a request that cannot begin (an
* unparseable script, an invalid meta block). Once it returns a
* {@link WorkflowRun}, `result` NEVER rejects — every failure resolves with
* `stopReason: 'error'` (or `'cancelled'`).
* - The `workflow/*` events fire through {@link emitWorkflowEvent} (data
* snapshots, per-listener containment); `workflow/end` fires exactly once
* per started run, after `result` is settled or as it settles.
* - `dispose()` reaches quiescence within a bounded grace: it cancels, waits
* for the script to settle, and abandons a stuck script rather than
* hanging its caller (the engine documents what abandonment leaves behind).
*/
export abstract class WorkflowService extends Service {
constructor(ctx: Context) {
super(ctx, 'workflows')
}
/**
* Parse and execute a workflow script.
* @param request - the script, its `args`, the parent agent, and an
* optional cancel signal.
* @returns the live run; its `result` resolves when the script settles.
*/
abstract start(request: WorkflowStartRequest): WorkflowRun
/**
* Emit one `workflow/*` lifecycle event with PER-LISTENER containment:
* dispatch each subscriber individually and log (never propagate) a thrown
* one, so one bad subscriber can neither fail the engine mid-run, surface as
* an unhandled rejection on a detached settle hook, nor starve the listeners
* registered after it (cordis `emit` halts on the first throw — same
* guarantee as the subagent seam's lifecycle emits).
* @param name - the `workflow/*` event to dispatch.
* @param args - the event's payload, matching its declared signature.
*/
protected emitWorkflowEvent(name: WorkflowEventName, ...args: unknown[]): void {
for (const callback of this.ctx.events.dispatch('emit', [name, ...args])) {
try {
// The declared workflow/* signatures are all void-returning emits; the
// dispatch callback applies the payload tuple.
;(callback as (...payload: unknown[]) => void)(...args)
} catch (error: unknown) {
this.ctx.logger.warn(`workflow: ${name} listener threw: ${String(error)}`)
}
}
}
}
export default WorkflowService

View File

@@ -0,0 +1,154 @@
/**
* Workflow seam vocabulary: the request/run/result types a workflow engine
* consumes and produces, plus the payload shapes of the `workflow/*` events.
* Types only (plus the id-brand factory), per the package convention.
*
* @module @deepseek-ai/dsh-workflow/types
*/
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { Agent, AgentId } from '@deepseek-ai/dsh-agent'
/** Identifies one workflow run. */
export type WorkflowRunId = Branded<'WorkflowRunId'>
/** Brand a string as a {@link WorkflowRunId}. */
export function WorkflowRunId(id: string): WorkflowRunId {
return id as WorkflowRunId
}
/**
* One phase declared in a script's `meta.phases` (progress vocabulary only —
* phases group agents in observers/UIs; they impose no execution structure).
*/
export interface WorkflowPhase {
/** The phase title; `phase()` calls match against it by exact string. */
title: string
/** Optional one-line description of what the phase does. */
detail?: string
/** Optional model override this phase is expected to use (informational). */
model?: string
}
/**
* The script's `export const meta` block, validated by the engine before the
* body runs. `name`/`description` are required; the rest is optional
* annotation. Matches the Claude Code dynamic-workflows script format.
*/
export interface WorkflowMeta {
/** Short kebab-case workflow name (display + persistence key). */
name: string
/** One-line description of what the workflow does. */
description: string
/** Optional guidance on when this workflow applies (shown in listings). */
whenToUse?: string
/** Optional phase declarations matched by `phase()` calls. */
phases?: WorkflowPhase[]
}
/**
* What a caller asks for when starting a workflow run. `parent` is REQUIRED —
* every `agent()` the script spawns is attributed to it (cwd, lineage, depth
* flow through the subagent seam). `args` must be plain host-realm JSON data;
* the engine exposes it to the script as the `args` global.
*/
export interface WorkflowStartRequest {
/** The full script text: `export const meta = {...}` + a plain-JS body. */
script: string
/** Optional input exposed verbatim to the script as the `args` global. */
args?: unknown
/** The agent on whose behalf the run executes (parent of every child). */
parent: Agent
/** Cancels the run when aborted (the tool's `exec.signal`). */
signal?: AbortSignal
}
/**
* Why a run settled. CLOSED union (engine-owned, consumers may exhaust):
* `completed` = the script ran to its final `return`; `cancelled` = the run
* was cancelled (caller `cancel()`/signal); `error` = the script threw, a
* fatal `WorkflowError` propagated, or the result failed materialization.
*/
export type WorkflowStopReason = 'completed' | 'cancelled' | 'error'
/**
* The outcome of one run, resolved by {@link WorkflowRun.result}. `value` is
* the script's materialized return value (plain host-realm JSON data; `null`
* when the script returned `undefined`) — meaningful only for `completed`.
* A non-`completed` reason carries the failure in `error`; the consumer maps
* it to an `isError` tool result rather than reporting partial output.
*/
export interface WorkflowResult {
/** The script's return value (host JSON data; `null` for no return). */
value: unknown
/** Why the run settled. */
stopReason: WorkflowStopReason
/** The failure message (present iff `stopReason` is not `completed`). */
error?: string
/** How many `agent()` calls the run started (across its whole lifetime). */
agentsStarted: number
}
/**
* The handle the consumer holds while a script executes. The consumer awaits
* `result`, may `cancel` mid-flight, and MUST `dispose` on every path.
* `result` does NOT reject — a script failure resolves with `stopReason:
* 'error'` — so the consumer maps a non-`completed` reason to an `isError`
* result. `dispose()` cancels, then waits a bounded grace for the script to
* settle before abandoning it (the engine documents the abandonment
* semantics); it never hangs on a stuck script.
*/
export interface WorkflowRun {
readonly id: WorkflowRunId
/** The validated meta block (available before the body runs). */
readonly meta: WorkflowMeta
readonly result: Promise<WorkflowResult>
/** Cancel the run: children abort, pending hooks reject, the script dies at its next await. */
cancel(reason?: string): void
/** Cancel + bounded-grace settle; safe to call on every path (idempotent). */
dispose(): Promise<void>
}
/** Identifying detail for a run, carried by every `workflow/*` event (a data snapshot, never the live run). */
export interface WorkflowRunInfo {
/** The run's id. */
id: WorkflowRunId
/** The run's validated meta block. */
meta: WorkflowMeta
}
/** One `agent()` call's identity within a run (the `workflow/agent-start` payload). */
export interface WorkflowAgentInfo {
/** 1-based sequence number of this `agent()` call within the run. */
seq: number
/** The display label (the `label` option, or a prompt snippet). */
label: string
/** The phase this agent belongs to (the `phase` option, else the current `phase()` title). */
phase?: string
/** The child agent's id on the subagent seam. */
childId: AgentId
}
/** How one `agent()` call settled: clean result, child failure (script sees `null`), or run cancellation. */
export type WorkflowAgentOutcome = 'completed' | 'failed' | 'cancelled'
/** One `agent()` call's settlement (the `workflow/agent-end` payload). */
export interface WorkflowAgentEndInfo extends WorkflowAgentInfo {
/** How the call settled. */
outcome: WorkflowAgentOutcome
}
/**
* A settled run's outcome as event data (the `workflow/end` payload): the
* {@link WorkflowResult} minus `value` (a listener observing outcomes must not
* receive a mutable alias of the caller's result value; a consumer that needs
* the value holds the run and awaits `result`).
*/
export interface WorkflowResultInfo {
/** Why the run settled. */
stopReason: WorkflowStopReason
/** The failure message (present iff `stopReason` is not `completed`). */
error?: string
/** How many `agent()` calls the run started. */
agentsStarted: number
}

View File

@@ -0,0 +1,86 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import WorkflowServiceDefault, {
isFatalWorkflowError,
WorkflowError,
WorkflowRunId,
WorkflowService,
} from '../src/index.ts'
import type { WorkflowRun, WorkflowRunInfo, WorkflowStartRequest } from '../src/index.ts'
/** A minimal concrete subclass exposing the protected emit helper for tests. */
class StubEngine extends WorkflowService {
start(request: WorkflowStartRequest): WorkflowRun {
void request
throw new Error('not under test')
}
emit(name: Parameters<WorkflowService['emitWorkflowEvent']>[0], ...args: unknown[]): void {
this.emitWorkflowEvent(name, ...args)
}
}
const INFO: WorkflowRunInfo = { id: WorkflowRunId('run-1'), meta: { name: 'w', description: 'd' } }
describe('dsh-workflow (interface)', () => {
it('WorkflowRunId brands a string (identity at runtime)', () => {
expect(WorkflowRunId('abc')).toBe('abc')
})
it('WorkflowError carries code + fatal (default true) and reads as a HarnessError', () => {
const error = new WorkflowError('cap hit', 'AGENT_CAP')
expect(error.code).toBe('AGENT_CAP')
expect(error.fatal).toBe(true)
expect(error.name).toBe('WorkflowError')
const soft = new WorkflowError('advisory', 'ITEM_CAP', { fatal: false })
expect(soft.fatal).toBe(false)
})
it('isFatalWorkflowError: true only for a fatal WorkflowError', () => {
expect(isFatalWorkflowError(new WorkflowError('x', 'CANCELLED'))).toBe(true)
expect(isFatalWorkflowError(new WorkflowError('x', 'CANCELLED', { fatal: false }))).toBe(false)
expect(isFatalWorkflowError(new Error('plain'))).toBe(false)
expect(isFatalWorkflowError('string')).toBe(false)
})
it('registers as ctx.workflows and unregisters when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
const fiber = await ctx.plugin(StubEngine)
expect(ctx.get('workflows')).toBeInstanceOf(StubEngine)
await fiber.dispose()
expect(ctx.get('workflows')).toBeUndefined()
})
it('emitWorkflowEvent dispatches to every listener with the payload tuple', async () => {
const ctx = new Context()
await ctx.plugin(StubEngine)
const seen: unknown[][] = []
ctx.on('workflow/log', (info, message) => { seen.push([info, message]) })
ctx.on('workflow/agent-start', (info, agent) => { seen.push([info, agent]) })
const engine = ctx.workflows as StubEngine
engine.emit('workflow/log', INFO, 'hello')
engine.emit('workflow/agent-start', INFO, { seq: 1, label: 'l', childId: 'c' })
expect(seen).toEqual([
[INFO, 'hello'],
[INFO, { seq: 1, label: 'l', childId: 'c' }],
])
})
it('contains a throwing listener PER LISTENER: later listeners still run, nothing propagates', async () => {
const ctx = new Context()
await ctx.plugin(StubEngine)
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => ctx.logger)
const reached: string[] = []
ctx.on('workflow/phase', () => { throw new Error('bad listener') })
ctx.on('workflow/phase', (_info, title) => { reached.push(title) })
const engine = ctx.workflows as StubEngine
expect(() => { engine.emit('workflow/phase', INFO, 'Scan') }).not.toThrow()
expect(reached).toEqual(['Scan'])
expect(warn).toHaveBeenCalledOnce()
expect(String(warn.mock.calls[0]![0])).toContain('workflow/phase listener threw')
})
it('has the expected export surface (default = the abstract service class)', () => {
expect(WorkflowServiceDefault).toBe(WorkflowService)
})
})

View File

@@ -0,0 +1,27 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../core/agent"
},
{
"path": "../../util/brand"
},
{
"path": "../../llm/llm"
}
]
}