Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
994f6a8
Copy conductor.ai.agents from Agentspan; fix eager langchain import
Jul 7, 2026
9615194
Merge Agentspan dependencies into pyproject.toml as optional extras; …
Jul 7, 2026
7f7e19b
Migrate Agentspan test suite to tests/ai/; register pytest markers
Jul 7, 2026
4a4f609
Migrate Agentspan examples, scripts, and e2e tests
Jul 7, 2026
a5a0f43
Add agent import smoke test and [agents]-extra test job to PR CI
Jul 7, 2026
dd3f6bf
Add agents documentation; update README with extras install instructions
Jul 7, 2026
51d032b
Change AgentConfig default server_url from :6767 to :8080
Jul 8, 2026
5bd0558
Replace port from 6767 to 8080
Jul 8, 2026
7576c2d
Replace port from 6767 to 8080
Jul 8, 2026
6fb36f4
Replace port from 6767 to 8080
Jul 8, 2026
2c3420c
Move agent tests under tests/unit/ai and tests/integration/ai
Jul 8, 2026
1c0b332
Migrate kitchen-sink test suite from Agentspan tests/ root
Jul 8, 2026
0c4c225
Add nightly agent e2e/integration CI against the released server JAR
Jul 8, 2026
6da159b
Run agent e2e workflow on every PR instead of nightly
Jul 8, 2026
a3f0a4e
Drop integration tests from the agent e2e workflow
Jul 8, 2026
ced884a
Use `fork` for spawning multiple process for now
Jul 8, 2026
68d407e
Move agents exception leaf to conductor/client/ai (shimmed)
Jul 8, 2026
9e5a0bc
Move schedule client/models/errors to conductor/client/ai (shimmed)
Jul 8, 2026
2325b2e
Extract AgentApiClient transport into conductor/client/ai
Jul 8, 2026
ae04de6
Consolidate agent API auth onto Configuration token machinery
Jul 8, 2026
73c6bd9
Docs: canonical conductor.client.ai paths for agent clients
Jul 8, 2026
8c3c5fd
Add spawn-safety foundation: FunctionRef, SpawnSafetyError, probe, as…
Jul 8, 2026
d0778c9
Make tool workers spawn-safe: ToolWorkerEntry replaces the make_tool_…
Jul 8, 2026
58afe7f
Make code-execution tools spawn-safe: CodeExecutionEntry / ExecutorTo…
Jul 8, 2026
d1a9f03
Make the 12 system workers spawn-safe: entry classes replace AgentRun…
Jul 8, 2026
1b03c81
Always use `spawn` to start multiple process
Jul 8, 2026
373f868
Fix spawn-safety gaps surfaced by live e2e validation
Jul 8, 2026
770973e
Make framework workers spawn-safe: GraphWorkerEntry + PassthroughWork…
Jul 9, 2026
e714961
Fix the 2 CI spawn-probe failures: container-attr hop + Guardrail tra…
Jul 9, 2026
a7ff8ec
S1: scheduler transport hand-fixes — PUT pause/resume with 405→GET fa…
Jul 9, 2026
83dae07
S2: schedule lifecycle as concrete SchedulerClient methods; mapping l…
Jul 9, 2026
2735470
S3: SchedulerClient is the get/save/list source of truth; AgentSchedu…
Jul 9, 2026
8336627
S4: docs — scheduler as the one schedule surface; verb-compat, deprec…
Jul 9, 2026
9d128f5
S5: delete AgentScheduleClient — no backward compatibility
Jul 9, 2026
3d591d0
S6: docs — AgentScheduleClient removal (no backward compatibility)
Jul 9, 2026
cdbd76b
Clean redundant typecheck
Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
139 changes: 139 additions & 0 deletions .github/workflows/agent-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: Agent E2E

# Runs the agent e2e suites (e2e/) against the released Agentspan
# server JAR — a full Conductor server with the agent runtime baked in.
# tests/integration/ai stays manual-only (same as upstream Agentspan,
# which never ran its tests/integration in CI): run it locally against
# a live server with `pytest tests/integration/ai`.
#
# These tests call real LLMs via the OPENAI_API_KEY / ANTHROPIC_API_KEY
# repo secrets. Fork PRs cannot see repo secrets, so for them the run
# fails at the silently-empty guard rather than passing vacuously.

on: [pull_request, workflow_dispatch]

concurrency:
group: agent-e2e-${{ github.ref }}
cancel-in-progress: true

env:
AGENTSPAN_VERSION: "0.4.0" # pinned server/CLI release — bump deliberately

jobs:
agent-e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
AGENTSPAN_SERVER_URL: http://localhost:8080/api
AGENTSPAN_CLI_PATH: ${{ github.workspace }}/agentspan
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

- name: Cache server JAR
id: jar_cache
uses: actions/cache@v4
with:
path: agentspan-server.jar
key: agentspan-server-${{ env.AGENTSPAN_VERSION }}

- name: Download server JAR from release
if: steps.jar_cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "v${AGENTSPAN_VERSION}" --repo agentspan-ai/agentspan \
--pattern "agentspan-server-${AGENTSPAN_VERSION}.jar" --output agentspan-server.jar

- name: Download CLI binary from release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "v${AGENTSPAN_VERSION}" --repo agentspan-ai/agentspan \
--pattern "agentspan_linux_amd64" --output agentspan
chmod +x agentspan

- name: Install SDK + test deps
run: |
python -m pip install --upgrade pip
pip install -e '.[agents]'
pip install pytest pytest-asyncio pytest-xdist pytest-rerunfailures mcp-testkit

- name: Start mcp-testkit
run: |
mcp-testkit --transport http --port 3001 &
sleep 2

- name: Start server
run: |
java -jar agentspan-server.jar --server.port=8080 > server.log 2>&1 &

- name: Wait for server health
run: |
for i in $(seq 1 45); do
if curl -sf http://localhost:8080/health | grep -q '"healthy"[[:space:]]*:[[:space:]]*true'; then
echo "server healthy after ~$((i*2))s"; exit 0
fi
sleep 2
done
echo "::error::server failed to become healthy within 90s"
tail -100 server.log
exit 1

- name: Run e2e suites
run: |
mkdir -p results
pytest e2e/ -v --tb=short -n 3 --dist=loadgroup \
--junitxml=results/junit-e2e.xml

# e2e/conftest.py pytest.skip()s the whole session when the server is
# unreachable — without this guard a boot failure after the health gate
# (or a future gate regression) would yield a green job that ran nothing.
- name: Guard against silently-empty runs
if: always()
run: |
python - <<'EOF'
import glob
import sys
import xml.etree.ElementTree as ET

total = executed = 0
for path in glob.glob("results/junit-*.xml"):
root = ET.parse(path).getroot()
for suite in root.iter("testsuite"):
t = int(suite.get("tests", 0))
sk = int(suite.get("skipped", 0))
total += t
executed += t - sk
print(f"executed {executed}/{total} tests")
sys.exit(0 if executed > 0 else 1)
EOF

- name: Generate HTML report
if: always()
run: |
python e2e/report_generator.py results/junit-e2e.xml results/report.html || true

- name: Upload results
if: always()
uses: actions/upload-artifact@v4
with:
name: agent-e2e-results
path: |
results/
server.log
retention-days: 14
13 changes: 12 additions & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ jobs:
pip install -e .
pip install pytest pytest-cov coverage

- name: Verify agents import without extras installed
id: agent_base_import
continue-on-error: true
run: |
python -c "import conductor.ai.agents"

- name: Install agents extra
run: |
pip install -e '.[agents]'
pip install pytest-asyncio

- name: Prepare coverage directory
run: |
mkdir -p ${{ env.COVERAGE_DIR }}
Expand Down Expand Up @@ -91,5 +102,5 @@ jobs:
file: coverage.xml

- name: Check test results
if: steps.unit_tests.outcome == 'failure' || steps.bc_tests.outcome == 'failure' || steps.serdeser_tests.outcome == 'failure'
if: steps.unit_tests.outcome == 'failure' || steps.bc_tests.outcome == 'failure' || steps.serdeser_tests.outcome == 'failure' || steps.agent_base_import.outcome == 'failure'
run: exit 1
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ conductor-{language}/

#### 1.2 HTTP/API Layer

> **HAND-FIX policy:** the bundled scheduler API spec is out of date, so
> `scheduler_resource_api.py` carries hand-applied fixes marked with `HAND-FIX`
> comments (per-schedule pause/resume verbs are `PUT` — the OSS Conductor dialect,
> with a 405→`GET` fallback for Orkes servers living in `OrkesSchedulerClient`
> plus an optional `reason` query param and `get_schedule`
> `response_type='WorkflowSchedule'`). Never regenerate that file without
> re-applying them; `tests/unit/orkes/test_scheduler_resource_contract.py` fails
> loudly if a regeneration reverts them.
- [ ] Generate models from OpenAPI specification
- [ ] Generate resource API classes
- [ ] Implement ApiClient with:
Expand Down
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Canonical metrics mode: opt-in harmonized metric surface via `WORKER_CANONICAL_METRICS=true` -- [details](METRICS.md#detailed-technical-notes--unreleased)
- `MetricsSettings` gains `clean_directory` and `clean_dead_pids` for opt-in stale `.db` file cleanup (both default to `False`)
- `CONDUCTOR_MP_START_METHOD` env var to control the worker pool's multiprocessing start method
- `SchedulerClient` now carries the schedule lifecycle operations itself: `pause(reason=)`, `resume`, `delete`, `run_now`, `preview_next`, `reconcile` (declarative tri-state sync) — with typed errors (`ScheduleNotFound`, `InvalidCronExpression`, ...). `pause_schedule` gains an optional `reason=` (stored by OSS Conductor servers; ignored by Orkes servers)

### Changed

- **Multiprocessing start method now defaults to `spawn` on all platforms** (was `fork` everywhere except Windows). `fork` caused silently-restarting `SIGSEGV` worker subprocesses on macOS (exitcode `-11`) and fork-with-held-lock deadlocks on POSIX. Opt back in with `CONDUCTOR_MP_START_METHOD=fork`. Entrypoint scripts must use the standard `if __name__ == "__main__":` guard, and workers must be defined at module level
- **Multiprocessing start method now defaults to `spawn` on all platforms** (was `fork` everywhere except Windows). `fork` caused silently-restarting `SIGSEGV` worker subprocesses on macOS (exitcode `-11`) and fork-with-held-lock deadlocks on POSIX.
- Legacy metrics emit unchanged by default; no env var required
- `metrics_collector.py` is now a compatibility shim; `from conductor.client.telemetry.metrics_collector import MetricsCollector` continues to work
- **Breaking (scoped):** schedule client objects no longer expose the mapped `get`/`save`/`list_for_agent` wrappers — `SchedulerClient`'s native methods are the source of truth: `get(wire)` → `get_schedule(wire)`, `save(schedule, agent)` → `save_schedule(SaveScheduleRequest)`, `list_for_agent(agent)` → `get_all_schedules(workflow_name=agent)`. The module-level `schedules.list/get/save` functions (and their `ScheduleInfo` returns) are unchanged
- `get_schedule` returns a typed `WorkflowSchedule` (or `None` for missing schedules) instead of a raw camelCase dict, matching its declared annotation and [docs/SCHEDULE.md](docs/SCHEDULE.md); dict-consumers should switch to attribute access or `to_dict()`

### Removed

- **Breaking:** `AgentScheduleClient` (alias `ScheduleClient`) and `OrkesClients.get_agent_schedule_client()` are removed with no compatibility shim — `SchedulerClient` (via `OrkesClients.get_scheduler_client()`) is the one schedule client and carries the full lifecycle itself. Replace `AgentScheduleClient(scheduler_client, workflow_client)` constructions with `get_scheduler_client()`; `runtime.schedules_client()` and `AgentClient.schedules` now return that `SchedulerClient` directly (same methods, same shared instance)

### Fixed

- `@worker_task` workers are now picklable, making the decorator path work with the `spawn`/`forkserver` start methods (fixes `TypeError: cannot pickle '_thread.lock' object` and `PicklingError: it's not the same object as ...`; issues #264, #271): `Worker.api_client` is created lazily in the worker process, runtime state (locks, pending async tasks, background loop) is excluded from pickling and rebuilt in the child, and decorated functions are pickled as importable references resolved in the child
- `TaskHandler.start_processes()` no longer hangs the interpreter when a worker fails to start (e.g., unpicklable state under `spawn`); it now cleans up already-started subprocesses and raises with actionable guidance
- Worker processes killed by a signal now log a diagnostic hint (signal number, `PYTHONFAULTHANDLER=1` guidance) instead of restarting silently
- Per-schedule pause/resume now work on both Conductor server families: the client sends `PUT` (the OSS Conductor dialect — the spec-generated `GET` failed there) and transparently falls back to `GET` on a 405 for Orkes servers, caching the working dialect per client instance
4 changes: 0 additions & 4 deletions METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,10 +378,6 @@ unreleased metrics harmonization work. For a summary, see the project
`create_metrics_collector` (the per-worker path) is non-destructive and
only ensures the directory exists, so spawned/restarted workers never wipe
live sibling metrics.
- `CONDUCTOR_MP_START_METHOD` env var (`spawn` / `fork` / `forkserver`;
default `fork` on POSIX, `spawn` on Windows) to control the worker pool's
multiprocessing start method (motivated by a `prometheus_client` lock-fork
deadlock).
- Harness manifest sets `WORKER_CANONICAL_METRICS=true`; `harness/main.py`
logs which collector is active.

Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ If you find [Conductor](https://github.com/conductor-oss/conductor) useful, plea
* [Monitoring with Metrics](#monitoring-with-metrics)
* [Managing Workflow Executions](#managing-workflow-executions)
* [AI & LLM Workflows](#ai--llm-workflows)
* [AI Agents](#ai-agents)
* [Why Conductor?](#why-conductor)
* [Examples](#examples)
* [Documentation](#documentation)
Expand Down Expand Up @@ -67,6 +68,13 @@ pip install conductor-python

> **Already in a virtual environment?** Skip the `venv` step and run `pip install conductor-python` directly. On macOS, Windows, or in containers where system Python is not locked down, you can also install globally.

Building durable AI agents instead? Install the `agents` extra (or a per-framework extra —
see [AI Agents](#ai-agents)):

```shell
pip install 'conductor-python[agents]'
```

## 60-Second Quickstart

**Step 1: Create a workflow**
Expand Down Expand Up @@ -407,6 +415,35 @@ python examples/rag_workflow.py document.pdf "What are the key findings?"

---

## AI Agents

Beyond the workflow-embedded LLM calls above, `conductor-python` also ships a durable
agent-authoring layer — `Agent`, `AgentRuntime`, `tool`, guardrails, handoffs, and multi-agent
strategies — where the agent itself compiles into a Conductor workflow that runs on the server,
with retries, durable state, streaming, and human-in-the-loop pauses built in.

```shell
pip install 'conductor-python[agents]'
```

```python
from conductor.ai.agents import Agent, AgentRuntime

agent = Agent(name="greeter", model="anthropic/claude-sonnet-4-6",
instructions="You are a friendly assistant.")

with AgentRuntime() as runtime:
result = runtime.run(agent, "Say hello.")
print(result.output)
```

Framework integrations (LangChain, LangGraph, Google ADK, the OpenAI Agents SDK, Claude Agent SDK)
are optional extras — see [docs/agents/getting-started.md](docs/agents/getting-started.md) for
per-framework install instructions, and [examples/agents/](examples/agents/) for 270+ runnable
examples. Full docs: [docs/agents/](docs/agents/).

---

## Why Conductor?

| | |
Expand Down Expand Up @@ -467,6 +504,7 @@ End-to-end examples covering all APIs for each domain:
| [Secrets](docs/SECRET_MANAGEMENT.md) | Secret storage |
| [Prompts](docs/PROMPT.md) | AI/LLM prompt templates |
| [Integrations](docs/INTEGRATION.md) | AI/LLM provider integrations |
| [AI Agents](docs/agents/README.md) | Durable agent authoring: `Agent`, `AgentRuntime`, tools, guardrails, handoffs |
| [Metrics](METRICS.md) | Prometheus metrics collection |
| [Examples](examples/README.md) | Complete examples catalog |

Expand Down
28 changes: 28 additions & 0 deletions docs/SCHEDULE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ Operations for controlling schedule execution state.
| `resume_schedule()` | `PUT /api/scheduler/schedules/{name}/resume` | Resume a specific schedule | [Example](#resume-schedule) |
| `resume_all_schedules()` | `PUT /api/scheduler/schedules/resume` | Resume all schedules | [Example](#resume-all-schedules) |

> **Verb compatibility:** OSS Conductor maps per-schedule pause/resume as `PUT`;
> Orkes Conductor servers map them as `GET`. The client sends `PUT` first and
> transparently falls back to `GET` on a 405, remembering the working dialect for
> the rest of the client's lifetime — both server families work without configuration.
> The optional `reason` parameter on `pause_schedule` is stored by OSS servers only.

## Schedule Lifecycle Helpers

Beyond the endpoint methods above, `SchedulerClient` carries higher-level lifecycle
operations (shared with the agents layer). They raise typed errors
(`ScheduleNotFound`, `InvalidCronExpression`, `ScheduleNameConflict` from
`conductor.client.ai.schedule_errors`) instead of raw `ApiException`.

| Method | Signature | Description |
|--------|-----------|-------------|
| `pause()` | `(wire_name, reason=None)` | Pause with typed errors |
| `resume()` | `(wire_name)` | Resume with typed errors |
| `delete()` | `(wire_name)` | Delete with typed errors |
| `preview_next()` | `(cron, n=5, start_at=None, end_at=None) -> list[int]` | Next `n` epoch-ms fire times |
| `run_now()` | `(info: ScheduleInfo) -> str` | Fire the schedule's workflow once, returns execution id |
| `reconcile()` | `(workflow_name, desired: list[Schedule] \| None)` | Declarative sync: `None` no-op, `[]` purge, `[...]` upsert+prune |

Reads, writes, and lists have no duplicated helper forms — `get_schedule()`,
`save_schedule()`, and `get_all_schedules()` are the source of truth.

## Schedule Execution APIs

APIs for managing and querying schedule executions.
Expand Down Expand Up @@ -169,6 +194,9 @@ Pause a specific schedule to stop executions.
```python
scheduler_client.pause_schedule("daily_order_processing")
print("Schedule paused")

# Optionally record why (stored by OSS Conductor servers; ignored by Orkes servers):
scheduler_client.pause_schedule("daily_order_processing", reason="maintenance window")
```

#### Pause All Schedules
Expand Down
7 changes: 0 additions & 7 deletions docs/WORKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,13 +279,6 @@ Requirements under `spawn` (standard Python multiprocessing rules):
- `configuration`, `metrics_settings`, and `event_listeners` passed to
`TaskHandler` must be picklable

To opt back into the previous behavior (e.g., a Linux deployment relying on
fork's copy-on-write memory sharing):

```shell
export CONDUCTOR_MP_START_METHOD=fork # spawn (default) | fork | forkserver
```

If a worker process dies from a signal repeatedly at startup, set
`PYTHONFAULTHANDLER=1` to capture the crashing stack trace from the child.

Expand Down
39 changes: 39 additions & 0 deletions docs/agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Agentspan Python SDK

> Ships as part of [`conductor-python`](https://pypi.org/project/conductor-python/) — install with the `agents` extra
> (`pip install 'conductor-python[agents]'`) — you're in the right place.

Long-running, dynamic plan-execute, and event-driven AI agents in Python. You write plain Python; Agentspan compiles your agent into a Conductor workflow that runs on a server — with automatic retries, durable state, human-in-the-loop pauses, streaming, scheduling, dynamic plan-execute, and full execution history.

```python
from conductor.ai.agents import Agent, AgentRuntime

agent = Agent(name="greeter", model="anthropic/claude-sonnet-4-6",
instructions="You are a friendly assistant.")

with AgentRuntime() as runtime:
result = runtime.run(agent, "Say hello.")
print(result.output)
```

## Docs

- [Getting started](getting-started.md) — install, env vars, and a running agent in under 30 seconds.
- [Writing agents](writing-agents.md) — the `Agent` class and `@agent`, tools, multi-agent strategies, handoffs, guardrails, termination, callbacks, streaming + HITL, schedules, stateful and instance agents.
- [Framework agents](framework-agents.md) — run agents authored in the OpenAI Agents SDK, LangChain, LangGraph, or the Claude Agent SDK.
- [Advanced](advanced.md) — runtime config, the control-plane `AgentClient`, deploy vs serve vs run vs plan, structured output, credentials, plans (`PLAN_EXECUTE`), skills.
- [API reference](api-reference.md) — the public API surface in one place.

## Import surface

Everything public is importable from `conductor.ai.agents`:

```python
from conductor.ai.agents import Agent, AgentRuntime, tool, agent
```

A small OpenAI-Agents-compatible shim is also exposed at the top level:

```python
from conductor.ai import Runner, function_tool # drop-in for `agents.Runner`
```
Loading
Loading