mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* feat(extensions): add opt-in script workflow MVP * fix(workflows): clarify timeout handling and stabilize request identity * fix(ci): register workflow catalog and trim inherited guidance * fix(workflows): retain launch ownership and resolve injected input anchors
236 lines
12 KiB
Markdown
236 lines
12 KiB
Markdown
# Script workflows (Phase 1)
|
||
|
||
An opt-in packaged extension that runs inspectable JavaScript orchestration in the
|
||
background. Scripts call full DeerFlow Agents through the public `AgentRuns`
|
||
capability. A chat tool or authenticated plugin action returns a workflow run ID
|
||
promptly; **Script workflows** shows the script, argument hashes, execution limits,
|
||
progress, child conversation/run identities, and final result.
|
||
|
||
Phase 1 has no saved definition library, resume, authenticated output sign-off,
|
||
continuation serialization, automatic restart replay, or hard monetary budget.
|
||
It therefore does not complete the inspection-report scenario requiring human
|
||
approval between stages. Agent execution success, a script verdict and human
|
||
approval are separate facts.
|
||
|
||
## Install and enable
|
||
|
||
Requires a POSIX Gateway (Linux/macOS), Python 3.12+, the public extension API 0.2.5,
|
||
and a **single Gateway process per storage file**. Windows execution is unsupported.
|
||
The package holds an exclusive process lock; another process using that same file
|
||
fails service startup rather than recovering another worker's live workflows.
|
||
Use only a trusted operator-installed package.
|
||
|
||
Pinned QuickJS 1.19.4 provides CPython 3.12 wheels for macOS universal2 and Linux
|
||
x86_64. Python 3.13/3.14 and Linux arm64 installations build its source distribution:
|
||
install a C compiler and matching Python development headers first (for example,
|
||
Xcode Command Line Tools on macOS, or `build-essential` plus the matching
|
||
`python3-dev` package on Debian/Ubuntu). Source-build compatibility depends on your
|
||
Python/toolchain; use CPython 3.12 on a wheel-supported platform if the build fails.
|
||
This also applies to backend development dependency installation and arm64 containers.
|
||
|
||
From the repository root:
|
||
|
||
```sh
|
||
make extension-install SOURCE="$(pwd)/examples/deerflow-extension-workflows"
|
||
```
|
||
|
||
Set the installed plugin's operator-owned `config.yaml` entry:
|
||
|
||
```yaml
|
||
plugins:
|
||
- name: workflows
|
||
use: deerflow_extension_workflows:install
|
||
enabled: true
|
||
config:
|
||
enabled: true
|
||
storage_path: /absolute/deployment-owned/workflows.sqlite
|
||
agents: [lead_agent] # Add explicitly authorized existing Custom Agent names.
|
||
concurrency: 2
|
||
global_concurrency: 4
|
||
max_calls: 16
|
||
max_workflows: 4
|
||
max_workflows_per_owner: 2
|
||
timeout_seconds: 300
|
||
admission_timeout_seconds: 10
|
||
cpu_seconds: 5
|
||
memory_bytes: 16777216
|
||
```
|
||
|
||
Find **Script workflows** in the workspace extension directory for the installation
|
||
guide. Its catalog listing does not indicate installation or activation; the operator
|
||
configuration above and Gateway runtime registration determine availability.
|
||
|
||
Restart Gateway. The namespace is `community.workflows`; the page is
|
||
`/workspace/extensions/community.workflows/runs`. The UI and tools are disabled by
|
||
default. `agents` restricts target selection in addition to normal host permissions;
|
||
it does not grant access to otherwise unauthorized models, tools or Agents.
|
||
Only supported host session/auth-disabled credentials supply a retained AgentRuns
|
||
handle; PAT/internal/channel callers cannot acquire one.
|
||
|
||
## Run an example
|
||
|
||
Open **Script workflows**, paste
|
||
[`examples/checks.workflow.js`](examples/checks.workflow.js), and enter:
|
||
|
||
```json
|
||
{"product":"Synthetic sample","formula":["ingredient A"],"claims":["sample claim"]}
|
||
```
|
||
|
||
Click **Launch workflow**. An accepted launch shows its run ID immediately;
|
||
if refreshing the list or details fails, use **Retry inspection**. Inspection retries
|
||
do not launch another workflow. Resubmitting unchanged script/arguments retains the
|
||
same request ID, including after a lost launch response; changing either creates a
|
||
new launch identity. The example sequences evidence collection, fans out two
|
||
checks, performs at most two review calls, and branches on a structured verdict.
|
||
It returns findings for human review, never a product-pass or approval claim.
|
||
This invokes your configured real Agents and can incur provider costs.
|
||
|
||
In chat, ask the lead to write an inspectable workflow script and launch it with
|
||
`launch_workflow`, retaining a stable `request_id`. Host tool names are derived
|
||
from the namespace. Use `inspect_workflow` on demand to inspect outcomes and
|
||
`cancel_workflow` to request cancellation; do not ask the lead to poll continuously.
|
||
The plugin page refreshes active progress every two seconds.
|
||
|
||
## Script API
|
||
|
||
A script is an async function **body**, not a Node module. No imports, module
|
||
loader, `process`, shell, filesystem, network, credentials, or Python callbacks
|
||
are exposed. Frozen `args` is a JSON object.
|
||
|
||
```js
|
||
const first = await agent({stepId:'first', agentRef:'lead_agent', task:args.task});
|
||
const checks = await parallel([
|
||
() => agent({stepId:'a',agentRef:'lead_agent',task:{evidence:first.output}}),
|
||
() => agent({stepId:'b',agentRef:'lead_agent',task:{evidence:first.output}})
|
||
]);
|
||
log('Two checks finished');
|
||
return checks.map(check => check.output);
|
||
```
|
||
|
||
`agent()` resolves to `{output, threadId, runId, status: 'success'}`. `output` is the
|
||
last attributable non-tool assistant answer: parsed JSON if valid, otherwise text
|
||
or content blocks. There is no provider-enforced structured output schema in this
|
||
API; validate verdicts in the script before branching. Every Agent call must be
|
||
awaited, including fan-out calls. Returning while child promises are outstanding
|
||
fails the workflow and interrupts tracked children.
|
||
|
||
`parallel()` takes lazy thunks and preserves order. The service, independently of
|
||
JavaScript promises, bounds admitted children per workflow and globally. A rejected
|
||
call rejects its promise; failures are not silently converted to null. Scripts may
|
||
catch ordinary failures and use explicit bounded revisions with **new** step IDs.
|
||
Limit faults, human-input waits, and uncertain admissions stop the supervisor even
|
||
if script code catches their promise rejection.
|
||
|
||
Step IDs contain 1–80 ASCII letters, digits, underscores or hyphens and cannot
|
||
repeat within a workflow. Attempts are currently always 1. `task` is JSON and
|
||
becomes an ordinary human message on a distinct, stably named owned child thread.
|
||
The host binds that thread's target Agent, checks current authorization on each
|
||
operation, and uses its normal agent assembly, models, tools, skills, memory and
|
||
checkpoints. The ledger records the requested agent reference and child IDs;
|
||
it does **not** claim to record a per-run effective assembly fingerprint.
|
||
|
||
## Limits and status
|
||
|
||
Limits are deployment-controlled, not writable by generated script or browser input.
|
||
Defaults are in the configuration above; ceilings are concurrency 4, global
|
||
concurrency 8, calls 32, active workflows 8 (4 per owner), wall time 600 seconds,
|
||
admission 20 seconds, cumulative worker CPU 10 seconds, JS memory 32 MiB.
|
||
|
||
Each JS engine entry has a 50 ms CPU interruption limit; stack is 256 KiB. The
|
||
isolated worker additionally has a cumulative POSIX CPU limit, no core dumps,
|
||
zero file-write size allowance, a temporary working directory and a minimal
|
||
credential-free environment. The parent kills/reaps it on termination. This is a
|
||
capability-restricted JS interpreter in a separate process, not a general OS
|
||
sandbox or a guarantee against native engine vulnerabilities.
|
||
|
||
IPC frames are bounded to 48 KiB. Script JSON encoding is at most 16,000 bytes,
|
||
arguments 4,000 bytes, task 8,000 bytes, each captured Agent output/final result
|
||
12,000 bytes. There are at most 20 progress messages of 160 JSON bytes each and
|
||
100 retained workflows per owner. Inspect responses are bounded to 60 KiB. Unicode
|
||
limits apply to the JSON encoding too, including escapes. Oversized results fail
|
||
explicitly instead of truncating evidence silently. Latest list returns 20 records;
|
||
keep a workflow run ID to inspect older retained records.
|
||
|
||
There is **no hard dollar-cost limit**. Call/time/CPU limits and ordinary host token
|
||
policies do not constitute a shared workflow currency budget. Unknown configuration
|
||
keys such as `max_cost_usd` are rejected. Add a generic, authoritative host budget
|
||
reservation/enforcement contract before advertising that guarantee.
|
||
|
||
| State | Meaning |
|
||
| --- | --- |
|
||
| queued / running | Supervised execution in this Gateway process |
|
||
| completed | Script and awaited child calls returned; output is unapproved task data |
|
||
| failed | Script, resource limit (including wall deadline), attribution, child execution or observation failed |
|
||
| cancelled | Stop requested and known active children interrupted or already terminal |
|
||
| waiting_input | Child has a graph interrupt or human-input artifact; downstream stopped |
|
||
| needs_reconciliation | Admission/cancellation outcome uncertain; inspect child identities before new work |
|
||
| interrupted | Supervisor externally cancelled, or unfinished record recovered after Gateway restart; no execution replay |
|
||
|
||
Wall-clock expiry reports `failed` with `Execution deadline exceeded` and cleans
|
||
up tracked children and the worker. A timeout after child admission reports child
|
||
observation failure and interrupts/observes the known run; only uncertain cleanup
|
||
escalates it to `needs_reconciliation`. Admission timeouts without a known run ID
|
||
retain the stable child thread identity for reconciliation.
|
||
|
||
Allowlist order does not change request identity; adding or removing allowed Agent
|
||
references does. Matching retries still return the retained record after restart.
|
||
|
||
Cancel is asynchronous and never rolls back side effects. The service retains
|
||
in-flight admission until its bounded response returns, records the actual child
|
||
run ID, then explicitly interrupts and observes that child. If admission times
|
||
out or authorization is revoked during cleanup, it retains `needs_reconciliation`
|
||
with stable thread/input/idempotency identity. It never automatically retries an
|
||
uncertain admission. Stable request IDs return an existing workflow record; they
|
||
never resume it or reexecute completed work. Changed scripts/args/limits with the
|
||
same request ID conflict. Phase 2 definition/config invalidation is not implemented.
|
||
|
||
Caller cancellation during launch does not abandon a committed record: the service
|
||
owns persistence through supervisor publication, including shutdown coordination.
|
||
A matching retry returns that same supervised record rather than an unowned queued
|
||
launch. This does not add execution replay or resume.
|
||
|
||
Result collection anchors on the visible human input, including the host's canonical
|
||
`__user` rewrite after dynamic context injection. Hidden system/memory reminders
|
||
cannot become the anchor; genuinely later human input still conflicts.
|
||
Result collection checks input message/run attribution and rejects an intervening
|
||
human turn, because `get_state()` is the thread's latest state, not immutable run
|
||
output. Results are copied before further script calls. Raw per-step input/results
|
||
are retained privately; public inspection returns a bounded final result and step
|
||
metadata. Child conversations remain accessible through normal host authorization.
|
||
|
||
At startup, unfinished records become `interrupted`; retained IDs do not recreate
|
||
a capability or arbitrary JS continuation. Gateway revokes handles during shutdown,
|
||
so cleanup may require reconciliation. Neither graph resume nor tool permission
|
||
approval is interpreted as authenticated approval of an artifact version.
|
||
|
||
## Verify
|
||
|
||
Backend CI installs QuickJS as a POSIX development dependency; the extension declares
|
||
its own pinned runtime dependency. No provider keys are required for these checks.
|
||
|
||
```sh
|
||
cd backend
|
||
uv run pytest tests/test_workflow_plugin.py -q
|
||
make test
|
||
make test-blocking-io
|
||
make format
|
||
```
|
||
|
||
Browser checks use a synthetic loopback fixture, not a deployed Gateway:
|
||
|
||
```sh
|
||
node examples/deerflow-extension-workflows/verify_browser.mjs
|
||
# If using an installed Chrome instead of downloaded Playwright Chromium:
|
||
PLAYWRIGHT_CHANNEL=chrome node examples/deerflow-extension-workflows/verify_browser.mjs
|
||
```
|
||
|
||
Install frontend dependencies first, or set `PLAYWRIGHT_MODULE` to an absolute
|
||
Playwright ES module path. The suite exercises launch, inspect, child navigation,
|
||
cancel, escaped output, deletion and disposal. Backend tests include the real
|
||
Gateway/LangGraph bridge with deterministic test nodes, plus controlled admission
|
||
races, limits, owner scope, service shutdown and restart records.
|
||
|
||
The implementation uses an original small JSON bridge. `deer-workflow` was evaluated
|
||
for API shapes but is not imported or copied: its dynamic Node module loader,
|
||
unbounded fan-out and external CLI Agent adapters are not used here.
|