index.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * Service Definition for the workflow capability seam. Service Providers execute orchestration scripts;
  3. * observe-only lifecycle events never expose run control.
  4. * @module @deepseek-ai/dsh-workflow
  5. */
  6. import { Context, Service } from '@deepseek-ai/cordis'
  7. import { HarnessError } from '@deepseek-ai/dsh-llm'
  8. import type {
  9. WorkflowAgentEndInfo,
  10. WorkflowAgentInfo,
  11. WorkflowResultInfo,
  12. WorkflowRunInfo,
  13. } from './types.ts'
  14. import type { WorkflowRun, WorkflowStartRequest } from './runtime-types.ts'
  15. export { WorkflowRunId } from './types.ts'
  16. export type {
  17. WorkflowAgentEndInfo,
  18. WorkflowAgentInfo,
  19. WorkflowAgentOutcome,
  20. WorkflowMeta,
  21. WorkflowPhase,
  22. WorkflowResult,
  23. WorkflowResultInfo,
  24. WorkflowRunInfo,
  25. WorkflowStopReason,
  26. } from './types.ts'
  27. export type { WorkflowRun, WorkflowStartRequest } from './runtime-types.ts'
  28. declare module '@deepseek-ai/cordis' {
  29. interface Context {
  30. workflowEngine: WorkflowEngine
  31. }
  32. interface Events {
  33. /**
  34. * A workflow run started — the script's meta block validated, the body
  35. * about to execute. Paired with {@link Events['workflow/end']}.
  36. * @param info - the run's identity snapshot (id + meta).
  37. * @mode emit
  38. */
  39. 'workflow/start'(info: WorkflowRunInfo): void
  40. /**
  41. * The script entered a phase (a `phase(title)` call) — progress grouping
  42. * for observers; no execution semantics.
  43. * @param info - the run's identity snapshot.
  44. * @param title - the phase title, verbatim.
  45. * @mode emit
  46. */
  47. 'workflow/phase'(info: WorkflowRunInfo, title: string): void
  48. /**
  49. * The script emitted a narration line (a `log(message)` call).
  50. * @param info - the run's identity snapshot.
  51. * @param message - the logged message, verbatim.
  52. * @mode emit
  53. */
  54. 'workflow/log'(info: WorkflowRunInfo, message: string): void
  55. /**
  56. * One `agent()` call established a published child run. Paired with
  57. * {@link Events['workflow/agent-end']} by `agent.seq`. A call that never
  58. * receives a published run from the provider emits neither
  59. * event in this pair.
  60. * @param info - the run's identity snapshot.
  61. * @param agent - the call's sequence number, label, phase, and child id.
  62. * @mode emit
  63. */
  64. 'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void
  65. /**
  66. * One `agent()` call settled (clean result, child failure, or run
  67. * cancellation). Paired with {@link Events['workflow/agent-start']} by
  68. * `agent.seq`, exactly once per started call on every stop path — on an
  69. * engine termination path the end is
  70. * engine-synthesized with outcome `'cancelled'`.
  71. * @param info - the run's identity snapshot.
  72. * @param agent - the call identity plus its outcome.
  73. * @mode emit
  74. */
  75. 'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void
  76. /**
  77. * A workflow run settled (any stop reason). Fired when
  78. * {@link WorkflowRun.result} resolves. Paired with
  79. * {@link Events['workflow/start']}.
  80. * @param info - the run's identity snapshot.
  81. * @param result - the outcome data (stop reason, error, agent count) —
  82. * deliberately WITHOUT the result value (see {@link WorkflowResultInfo}).
  83. * @mode emit
  84. */
  85. 'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void
  86. }
  87. }
  88. /** The full set of `workflow/*` event names {@link WorkflowEngine.emitWorkflowEvent} dispatches. */
  89. export type WorkflowEventName =
  90. | 'workflow/start'
  91. | 'workflow/phase'
  92. | 'workflow/log'
  93. | 'workflow/agent-start'
  94. | 'workflow/agent-end'
  95. | 'workflow/end'
  96. /**
  97. * Machine-routable fatal workflow failures: parse/meta/argument/schema errors,
  98. * resource caps, subagent infrastructure failures, unserializable boundary
  99. * values, and cancellation. An ordinary child failure resolves its item to
  100. * `null` and is not one of these fatal codes.
  101. */
  102. export type WorkflowErrorCode =
  103. | 'SCRIPT_PARSE'
  104. | 'META_INVALID'
  105. | 'INVALID_ARGUMENT'
  106. | 'UNSUPPORTED_OPTION'
  107. | 'UNSUPPORTED_SCHEMA'
  108. | 'AGENT_CAP'
  109. | 'ITEM_CAP'
  110. | 'AGENT_START'
  111. | 'AGENT_RESULT'
  112. | 'RESULT_UNSERIALIZABLE'
  113. | 'CANCELLED'
  114. /**
  115. * Typed error for workflow-seam failures. Extends {@link HarnessError}, so the
  116. * `code` is machine-routable taxonomy. `fatal` drives the combinator
  117. * discipline: `parallel()`/`pipeline()` re-throw a fatal error (a typo'd
  118. * option or a tripped cap must kill the script loudly), and reserve the
  119. * per-item `null` for child-run failures and ordinary in-stage script errors.
  120. * Every {@link WorkflowErrorCode} is fatal; the flag exists so the
  121. * distinction is explicit at every catch site rather than implied.
  122. */
  123. export class WorkflowError extends HarnessError {
  124. /** Whether combinators must propagate this error instead of nulling the item. */
  125. readonly fatal: boolean
  126. constructor(message: string, code: WorkflowErrorCode, options?: ErrorOptions & { fatal?: boolean }) {
  127. super(message, code, options)
  128. this.name = 'WorkflowError'
  129. this.fatal = options?.fatal ?? true
  130. }
  131. }
  132. /**
  133. * Whether combinators must re-throw `error` instead of mapping the item to `null`.
  134. * @param error - any thrown value; fatality is host `instanceof` (unforgeable from a script realm).
  135. * @returns true iff `error` is a {@link WorkflowError} whose `fatal` flag is set.
  136. */
  137. export function isFatalWorkflowError(error: unknown): boolean {
  138. return error instanceof WorkflowError && error.fatal
  139. }
  140. /**
  141. * Workflow Service Definition contract. Invalid requests throw before publication; a live
  142. * run is holder-owned, its result never rejects, and disposal waits for script
  143. * and child cleanup. Lifecycle
  144. * listener failures are contained, and `workflow/end` fires exactly once as the
  145. * result settles.
  146. */
  147. export abstract class WorkflowEngine extends Service {
  148. constructor(ctx: Context) {
  149. super(ctx, 'workflowEngine')
  150. }
  151. /**
  152. * Parse and execute a workflow script.
  153. * @param request - the script, its `args`, the parent agent, and an
  154. * optional cancel signal.
  155. * @returns the live run; its `result` resolves when the script settles.
  156. */
  157. abstract start(request: WorkflowStartRequest): WorkflowRun
  158. /**
  159. * Emit a lifecycle event while containing and logging each listener failure.
  160. * @param name - the `workflow/*` event to dispatch.
  161. * @param args - the event's payload, matching its declared signature.
  162. */
  163. protected emitWorkflowEvent(name: WorkflowEventName, ...args: unknown[]): void {
  164. for (const callback of this.ctx.events.dispatch('emit', [name, ...args])) {
  165. try {
  166. const returned: unknown = (callback as (...payload: unknown[]) => unknown)(...args)
  167. void Promise.resolve(returned).catch((error: unknown) => {
  168. this.ctx.logger.warn(`workflow: ${name} listener rejected: ${renderListenerError(error)}`)
  169. })
  170. } catch (error: unknown) {
  171. this.ctx.logger.warn(`workflow: ${name} listener threw: ${renderListenerError(error)}`)
  172. }
  173. }
  174. }
  175. }
  176. /**
  177. * Render any thrown value without violating listener containment.
  178. * @param error - any thrown value.
  179. * @returns `String(error)`, or a fixed label when even coercion throws.
  180. */
  181. function renderListenerError(error: unknown): string {
  182. try {
  183. return String(error)
  184. } catch {
  185. // String coercion itself may throw.
  186. return '[unrenderable thrown value]'
  187. }
  188. }
  189. export default WorkflowEngine