Skip to content

feat(workflow): engine API with fakes#96

Draft
Waishnav wants to merge 1 commit into
pr/dw-2-store-sandboxfrom
pr/dw-3-engine
Draft

feat(workflow): engine API with fakes#96
Waishnav wants to merge 1 commit into
pr/dw-2-store-sandboxfrom
pr/dw-3-engine

Conversation

@Waishnav

@Waishnav Waishnav commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

  • createWorkflowApi: agent/parallel/pipeline/phase/log/workflow + semaphore + ALS phase
  • executeWorkflow host entry; nest depth 1; isolation worktree hook
  • Unit tests with fake runProvider (no real LLM)

Stack

PR3 of 5 · base: pr/dw-2-store-sandbox

Test plan

  • tsx src/workflow-engine.test.ts
  • Cover: parallel null-on-throw, pipeline no barrier, phase ALS, worktree cwd, nest depth
  • npm run typecheck

Summary by CodeRabbit

  • New Features

    • Added workflow execution with agent calls, parallel processing, pipelines, phases, logging, cancellation, and nested workflows.
    • Added provider selection, concurrency controls, optional worktree isolation, replay support, and structured response extraction.
    • Added workflow error handling and execution metadata.
  • Tests

    • Added comprehensive integration coverage for concurrency, failures, cancellation, nesting, provider selection, phases, and isolation.
    • Expanded the test command to include the new workflow engine tests.

agent/parallel/pipeline/phase/log/workflow primitives, semaphore,
ALS phase, isolation worktree hook, nest depth 1, and executeWorkflow
against injectable runProvider for unit tests.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5192a094-3f2f-4428-a12a-7bcaeb28b058

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr/dw-3-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/workflow-api.ts
"isolation: 'worktree' requires createWorktree host support",
);
}
worktree = await deps.createWorktree({

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P0] Worktree creation happens before beginAgentCall, but the catch path always calls failAgentCall. If setup fails (non-Git workspace, missing Git, bad base SHA, collision), the original error is masked by Unknown workflow agent call. Begin a preparing row before fallible setup, or guard failure journaling with a callStarted flag.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed by stacked PR #101 (d5b00f5). The agent path now tracks whether beginAgentCall actually succeeded and only calls failAgentCall for an existing row. A worktree setup failure preserves the original error and still emits a typed agent_call_failed event. A regression test makes createWorktree throw and verifies that the error is not replaced by Unknown workflow agent call. This thread becomes obsolete once #101 is merged.

Comment thread src/workflow-api.ts
runId: deps.runId,
callIndex: index,
responseText: truncate(result.finalResponse, WORKFLOW_LIMITS.responseTextBytes),
structuredJson: structuredJson

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Please do not truncate structured JSON. A truncated payload is invalid JSON; replay then silently falls back to response text and changes the return type from object to string. Reject oversized structured results or persist them as an artifact/reference. Only free-form response text should be truncated.

Comment thread src/workflow-engine.ts
// Child surface: reuse parent agent/parallel/pipeline/phase/log/budget/workflow
// so callIndex + semaphore stay shared. Override args + meta for the child body.
const childApi: WorkflowApi = {
agent: input.parentApi.agent,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reusing the parent's agent closure also reuses the parent's meta and phase context, so a child's defaultProvider and phase semantics are not actually honored. Share only runtime state (semaphore, call counter, replay, signal, journal), then construct a child API with the child's own meta and args.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
src/workflow-engine.test.ts (4)

401-401: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

void createStubBudget; is a lint workaround, not a test.

Drop the import at Line 15 rather than keeping a no-op reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-engine.test.ts` at line 401, Remove the unused createStubBudget
import and delete the no-op void createStubBudget reference; no test logic
should remain solely to satisfy linting.

50-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

order is populated but never asserted.

The block header claims barrier semantics, yet nothing checks the recorded start/end interleaving. Either assert on order (e.g. all three start: entries precede the first end: at concurrency 4) or drop the variable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-engine.test.ts` around lines 50 - 78, Update the parallel
workflow test using the order variable in the createWorkflowApi test setup to
assert barrier semantics: verify all three start entries occur before the first
end entry when concurrency is 4. Keep the existing result and call-count
assertions, or remove order only if the interleaving behavior is no longer being
tested.

337-360: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Depth-2 case doesn't test what the comment describes.

The .then() chained on workflow(...) at Line 343 never executes because the promise rejects first, so the assertion passes for the right reason but via a misleading construct. Simplify to a parent that nests a mid script which itself nests, and drop the .then().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-engine.test.ts` around lines 337 - 360, Simplify the depth-2
test in the executeWorkflow case so the parent directly awaits workflow({
scriptPath: ... }) without chaining .then(). Keep resolveNestedSource returning
the mid script that performs the second nesting attempt, ensuring the nest_depth
rejection is exercised by the intended nested workflow path.

389-396: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cancellation test only covers the pre-flight path.

ac.abort() at Line 395 fires before api.agent, so runProvider is never invoked and the ac.abort() inside it at Line 390 is unreachable. Mid-flight cancellation — abort while the provider is running, and the post-provider throwIfCancelled in src/workflow-api.ts — is untested, as is the isCancelRequested(runId) journal branch. Consider adding a case that aborts during the provider call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-engine.test.ts` around lines 389 - 396, Add a separate
cancellation test that allows api.agent to enter runProvider, aborts the
controller from within or during that provider call, then resolves the provider
and asserts api.agent rejects with WorkflowEngineError. Cover the post-provider
cancellation check and ensure the runId journal is marked as cancelled via the
isCancelRequested(runId) path, while keeping the existing pre-flight test
unchanged.
src/workflow-engine.ts (2)

116-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dead try/catch — both branches rethrow the same error.

♻️ Simplify
-  try {
-    const result = await runWorkflowSandbox({
-      parsed,
-      api,
-      timeoutMs: options.timeoutMs ?? WORKFLOW_HOST_TIMEOUT_MS,
-    });
-    return {
-      result,
-      meta: parsed.meta,
-      callCount: api.getCallCount(),
-    };
-  } catch (error) {
-    if (error instanceof WorkflowEngineError) {
-      throw error;
-    }
-    throw error;
-  }
+  const result = await runWorkflowSandbox({
+    parsed,
+    api,
+    timeoutMs: options.timeoutMs ?? WORKFLOW_HOST_TIMEOUT_MS,
+  });
+  return {
+    result,
+    meta: parsed.meta,
+    callCount: api.getCallCount(),
+  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-engine.ts` around lines 116 - 132, Remove the redundant
try/catch surrounding runWorkflowSandbox in the workflow execution method; let
the await call and its existing return object propagate errors naturally,
preserving the special-case behavior only if it performs distinct handling
elsewhere.

149-153: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two sources of truth for the nesting limit.

WORKFLOW_MAX_NEST_DEPTH_LOCAL duplicates WORKFLOW_MAX_NEST_DEPTH from ./workflow-types.js, which src/workflow-api.ts already enforces in workflow(). If the shared constant ever changes, the API guard and the engine guard disagree. Import the shared constant instead.

Also applies to: 188-188

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-engine.ts` around lines 149 - 153, Replace the local
WORKFLOW_MAX_NEST_DEPTH_LOCAL usage in the workflow engine’s nesting checks with
the shared WORKFLOW_MAX_NEST_DEPTH imported from ./workflow-types.js, including
the additional check around the second referenced location. Remove the duplicate
local constant and keep the existing WorkflowEngineError behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/workflow-api.ts`:
- Around line 601-611: Update the option parsing around record.model,
record.effort, and record.provider to reject values when those properties are
present but not strings, matching the validation behavior of schema, isolation,
and writeMode. Preserve assignment for valid strings and ensure invalid values
throw a WorkflowEngineError instead of being silently omitted and falling back
to defaults.
- Around line 520-532: The nested workflow() path in the workflow function must
validate nameOrRef before calling deps.resolveNestedSource: accept only a string
or an object with a valid scriptPath string, and reject invalid or
workspace-escaping paths with WorkflowEngineError code "path". Document in the
resolveNestedSource contract that host resolvers must confine scriptPath to the
configured workspace root, while preserving valid nested workflow resolution.

---

Nitpick comments:
In `@src/workflow-engine.test.ts`:
- Line 401: Remove the unused createStubBudget import and delete the no-op void
createStubBudget reference; no test logic should remain solely to satisfy
linting.
- Around line 50-78: Update the parallel workflow test using the order variable
in the createWorkflowApi test setup to assert barrier semantics: verify all
three start entries occur before the first end entry when concurrency is 4. Keep
the existing result and call-count assertions, or remove order only if the
interleaving behavior is no longer being tested.
- Around line 337-360: Simplify the depth-2 test in the executeWorkflow case so
the parent directly awaits workflow({ scriptPath: ... }) without chaining
.then(). Keep resolveNestedSource returning the mid script that performs the
second nesting attempt, ensuring the nest_depth rejection is exercised by the
intended nested workflow path.
- Around line 389-396: Add a separate cancellation test that allows api.agent to
enter runProvider, aborts the controller from within or during that provider
call, then resolves the provider and asserts api.agent rejects with
WorkflowEngineError. Cover the post-provider cancellation check and ensure the
runId journal is marked as cancelled via the isCancelRequested(runId) path,
while keeping the existing pre-flight test unchanged.

In `@src/workflow-engine.ts`:
- Around line 116-132: Remove the redundant try/catch surrounding
runWorkflowSandbox in the workflow execution method; let the await call and its
existing return object propagate errors naturally, preserving the special-case
behavior only if it performs distinct handling elsewhere.
- Around line 149-153: Replace the local WORKFLOW_MAX_NEST_DEPTH_LOCAL usage in
the workflow engine’s nesting checks with the shared WORKFLOW_MAX_NEST_DEPTH
imported from ./workflow-types.js, including the additional check around the
second referenced location. Remove the duplicate local constant and keep the
existing WorkflowEngineError behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e825b213-63b5-48c4-9a8c-a3a3b5b97f65

📥 Commits

Reviewing files that changed from the base of the PR and between 930e8b3 and 775f7f0.

📒 Files selected for processing (4)
  • package.json
  • src/workflow-api.ts
  • src/workflow-engine.test.ts
  • src/workflow-engine.ts

Comment thread src/workflow-api.ts
Comment on lines +520 to +532
const workflow = async (...args: unknown[]): Promise<unknown> => {
if (nestDepth >= WORKFLOW_MAX_NEST_DEPTH) {
throw new WorkflowEngineError(
"nest_depth",
`workflow() nesting limited to ${WORKFLOW_MAX_NEST_DEPTH} level`,
);
}
if (!deps.resolveNestedSource || !deps.executeNested) {
throw new WorkflowEngineError("internal", "nested workflow() is not configured on this host");
}
const nameOrRef = args[0] as string | { scriptPath: string };
const childArgs = args[1];
const source = await deps.resolveNestedSource(nameOrRef);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether any host-side resolveNestedSource implementation confines scriptPath to the workspace root.
rg -nP -C6 'resolveNestedSource' --type=ts

Repository: Waishnav/devspace

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "tracked ts files count:"
git ls-files '*.ts' | wc -l

echo "search workflow-api references:"
fd -a 'workflow-api\.ts$' . || true

echo "search resolveNestedSource all files:"
rg -n -C 4 'resolveNestedSource|WORKFLOW_MAX_NEST_DEPTH|executeNested|WorkflowEngineError' . || true

echo "list relevant files:"
git ls-files | rg 'workflow|engine|test|src/workflow-api.ts' | head -200

Repository: Waishnav/devspace

Length of output: 24696


Constrain scriptPath before resolving nested workflows.

args[0] is passed directly to resolveNestedSource, and the test resolver reads ref.scriptPath without containment; this API does not enforce that host resolvers limit paths to the workspace. Add runtime validation for the string/ref shape and document that resolveNestedSource must confine scriptPath to the configured workspace root, using "path" errors when it does not.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-api.ts` around lines 520 - 532, The nested workflow() path in
the workflow function must validate nameOrRef before calling
deps.resolveNestedSource: accept only a string or an object with a valid
scriptPath string, and reject invalid or workspace-escaping paths with
WorkflowEngineError code "path". Document in the resolveNestedSource contract
that host resolvers must confine scriptPath to the configured workspace root,
while preserving valid nested workflow resolution.

Comment thread src/workflow-api.ts
Comment on lines +601 to +611
if (typeof record.label === "string") out.label = record.label;
if (typeof record.phase === "string") out.phase = record.phase;
if (record.schema !== undefined) {
if (!record.schema || typeof record.schema !== "object" || Array.isArray(record.schema)) {
throw new WorkflowEngineError("schema", "agent opts.schema must be an object");
}
out.schema = record.schema as object;
}
if (typeof record.model === "string") out.model = record.model;
if (typeof record.effort === "string") out.effort = record.effort;
if (typeof record.provider === "string") out.provider = record.provider;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-string option values are silently dropped.

schema, isolation, and writeMode throw on bad input, but a non-string provider (or model/effort) is silently ignored — agent("x", { provider: 123 }) quietly falls back to meta.defaultProvider instead of failing. Reject wrong-typed values that are present.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflow-api.ts` around lines 601 - 611, Update the option parsing around
record.model, record.effort, and record.provider to reject values when those
properties are present but not strings, matching the validation behavior of
schema, isolation, and writeMode. Preserve assignment for valid strings and
ensure invalid values throw a WorkflowEngineError instead of being silently
omitted and falling back to defaults.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant