Skip to content

feat: programmable dynamic workflows (facade, execute_loop, shared budget) + parallel-write safety fix#64

Merged
ZhiXiao-Lin merged 5 commits into
mainfrom
feat/dynamic-workflow
Jun 7, 2026
Merged

feat: programmable dynamic workflows (facade, execute_loop, shared budget) + parallel-write safety fix#64
ZhiXiao-Lin merged 5 commits into
mainfrom
feat/dynamic-workflow

Conversation

@ZhiXiao-Lin
Copy link
Copy Markdown
Contributor

Summary

Compose a Claude-Code-style dynamic workflow mechanism from the existing seams (AgentExecutor, WorkflowCheckpoint, BudgetGuard) — no new subsystem — plus two safety/correctness fixes.

Commits

  • fix(agent): parallel write batch must pass the safety gate — the parallel write fast path bypassed ToolSafetyGate (permission + skill restrictions). Now consults the gate itself (single source of truth); only fast-paths when every call is explicitly Allowed and unrestricted.
  • feat(orchestration): Workflow facade, execute_loop, shared budgetWorkflow facade (agent/parallel/phase/pipeline/log), execute_loop+LoopDecision (mandatory max_iterations), WorkflowBudget aggregating ledger; wires BudgetGuard through ChildRunContext; AgentSession::workflow() / workflow_with_token_budget(); also fixes agent_executor() to install parent_context (security/skill/workspace parity).
  • test: integration coverage#[ignore] real-LLM tests for the facade/phase/loop/budget (all 4 pass against a live provider).
  • feat(sdk): budgeted fan-out then refactor(sdk): fold into parallel(specs, budgetTokens?) — Node (napi Either) & Python (list|dict) return shapes; non-breaking optional budget arg.

Tests

  • cargo test -p a3s-code-core --lib1755 passed / 0 failed
  • Real-LLM integration (--test test_workflow_facade_real_llm -- --ignored) → 4 passed
  • Node npm test + Python smoke pass; clippy/fmt clean.

Docs are in a companion PR on A3S-Lab/a3s (apps/docs).

claude added 5 commits June 7, 2026 13:43
The parallel write fast path executed tools directly via the ToolExecutor,
bypassing ToolSafetyGate entirely — so with multiple write calls in one turn,
permission checks and skill restrictions were not enforced.

Make can_run_parallel_write_batch consult the gate itself (single source of
truth): only fast-path when, for every call, no active skill restriction forbids
the tool AND the permission checker explicitly Allows it. A missing checker
resolves to Ask (a Deny without a confirmation manager), so it correctly refuses.
This preserves the optimization for the explicit-allow case while closing the
bypass. Exposes ToolSafetyGate::{check_skill_restrictions,permission_decision}
as pub(crate).
…ed budget

Compose the existing combinators into a runtime-driven, Claude-Code-style
dynamic workflow mechanism, reusing the AgentExecutor / SessionStore /
WorkflowCheckpoint / BudgetGuard seams rather than inventing parallel ones.

- Workflow facade (orchestration/workflow.rs): a cheaply-clonable handle whose
  verbs agent/parallel/phase/pipeline each delegate to one combinator; phase is
  a named, resumable barrier ({root_id}/{index}:{name}) emitting WorkflowEvent
  milestones. Control flow lives in the host language.
- execute_loop + LoopDecision (combinators.rs): bounded loop-until-dry with a
  mandatory max_iterations hard cap.
- WorkflowBudget (orchestration/workflow_budget.rs): an aggregating BudgetGuard
  that sums token spend from every step into one shared ledger (soft cap).
- Wire BudgetGuard through ChildRunContext so a single guard spans a fan-out
  (closes a gap: budget was per-session_id only).
- AgentSession::workflow()/workflow_with_token_budget(): pre-wire executor,
  store, per-step events, session-derived root id, and the shared budget. Also
  fix agent_executor() to install parent_context (security/skill/workspace), so
  orchestrated steps are neither more nor less privileged than delegated ones.
- README: document the Workflow facade, loop, and shared budget.

Tests: full lib suite green; new unit + e2e coverage for every verb, resume,
loop caps, budget aggregation, and a real child-agent workflow step.
…allel)

Expose the shared workflow budget through both SDKs via a session method built
on AgentSession::workflow_with_token_budget(): run a fan-out where every child
agent feeds ONE token ledger and, once the cap is reached, further child LLM
calls are denied (a soft cap; the in-flight fan-out is never force-killed).
Returns the per-step outcomes plus the ledger snapshot.

- Node (napi):  session.workflowParallel(specs, budgetTokens?) -> { outcomes, budget }
- Python (pyo3): session.workflow_parallel(specs, budget_tokens=None) -> dict
- Regenerate node generated.d.ts; document both in the README SDK examples.

Both native modules build (napi build / maturin) and pass an offline smoke test
(empty fan-out takes no LLM path; correct ledger snapshot). Full orchestration
behavior is covered by the core crate's test suite.
- core/tests/test_workflow_facade_real_llm.rs (#[ignore], real-LLM gated like
  test_orchestration_real_llm.rs): the Workflow facade phase + milestones,
  execute_loop's max_iterations hard cap, session.workflow() running a real
  child agent with the shared ledger accumulating spend, and sequential budget
  enforcement (a step started after the cap is denied).
- sdk/node/test.mjs: offline workflowParallel shape check (empty fan-out, ledger
  snapshot) in the existing smoke run.
- sdk/python/tests/test_workflow_parallel.py: the same offline check for Python.

Verified: core suite green; the integration target compiles and registers its 4
ignored cases; node 'npm test' and the python smoke both pass offline.
…okens?)

Drop the awkwardly-named workflowParallel / workflow_parallel methods. The only
difference from parallel was a shared token budget, so make it an optional
argument on parallel itself instead of leaking the internal 'workflow' facade
name into the public API.

- Node: parallel(specs, budgetTokens?) -> Array<StepOutcomeObject> |
  WorkflowParallelResult (napi Either). No budget => the plain outcomes array
  (unchanged, non-breaking); a budget => { outcomes, budget } (ledger snapshot).
- Python: parallel(specs, budget_tokens=None) -> list | dict, same split.
- Update README SDK examples; node test.mjs and the renamed
  test_parallel_budget.py cover both the array and budgeted shapes offline.

Rebuilt both native modules (napi build / maturin); npm test and the python
smoke pass; clippy/fmt clean.
@ZhiXiao-Lin ZhiXiao-Lin merged commit 790e6c8 into main Jun 7, 2026
1 check passed
@ZhiXiao-Lin ZhiXiao-Lin deleted the feat/dynamic-workflow branch June 7, 2026 07:08
ZhiXiao-Lin pushed a commit that referenced this pull request Jun 7, 2026
- Rust (core + node/python SDK crates), Node package.json + lockfiles,
  Python SDK + bootstrap shim, and Cargo.lock all aligned to 3.5.0.
- 3.5.0 ships the programmable dynamic workflows merged in #64: Workflow facade,
  execute_loop, shared WorkflowBudget, parallel budget overload, plus the
  parallel-write safety-gate fix.
ZhiXiao-Lin added a commit that referenced this pull request Jun 7, 2026
- Rust (core + node/python SDK crates), Node package.json + lockfiles,
  Python SDK + bootstrap shim, and Cargo.lock all aligned to 3.5.0.
- 3.5.0 ships the programmable dynamic workflows merged in #64: Workflow facade,
  execute_loop, shared WorkflowBudget, parallel budget overload, plus the
  parallel-write safety-gate fix.

Co-authored-by: Claude <claude@anthropic.com>
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.

2 participants