Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b4e152b
Add durable-agent layer at src/agents exposed via /agents subpath exp…
Jul 9, 2026
6cf03bd
Port agent unit tests to jest (colocated __tests__) and migrate cli-bin
Jul 9, 2026
398b778
Migrate agent examples and docs under examples/agents and docs/agents
Jul 9, 2026
a79a313
Port agent e2e suites to jest under repo-root e2e/
Jul 9, 2026
10db074
Add per-PR agent-e2e workflow (release JAR + CLI + mcp-testkit)
Jul 9, 2026
4728434
Document the merged durable-agent layer
Jul 9, 2026
551b9d0
fix(ci): drop pip cache from agent-e2e setup-python
Jul 9, 2026
cbf3c0a
Switch port to 8080
Jul 9, 2026
6c75d48
fix(e2e): restore diag/codeOutputs names over-prefixed in suite 10 port
Jul 9, 2026
a00ecd7
feat(scheduler): PUT-first pause/resume with 405→GET fallback + typed…
Jul 9, 2026
7af89ab
feat(agents)!: delete ScheduleClient/SchedulerFetcher — schedules rid…
Jul 9, 2026
389cafa
feat(sdk): AgentClient + agent WorkflowClient live in sdk/clients, ex…
Jul 9, 2026
a410bad
docs: single-scheduler consolidation + agent clients in sdk/clients
Jul 9, 2026
8bc244e
refactor(sdk)!: AgentClient interface + OrkesAgentClient on the share…
Jul 14, 2026
80ba13f
feat(agents)!: AgentRuntime(configuration, settings); behavior-only A…
Jul 14, 2026
635f862
feat(agents): serve=deploy+serve with blocking option; variadic deplo…
Jul 14, 2026
089b39d
feat(agents)!: worker credentials via Task.runtimeMetadata, fail-clos…
Jul 14, 2026
876f91d
feat(agents): liveness monitor + wait deadline for stateful runs; swa…
Jul 14, 2026
05a56c7
docs(agents): CHANGELOG final surface; serve contract example prose; …
Jul 14, 2026
27db7e3
ci(agent-e2e): drop agentspan server lane, run conductor-oss only
Jul 14, 2026
59bd2cb
refactor(scheduler): borrow raw-call auth from the shared client's ac…
Jul 14, 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
  •  
  •  
  •  
154 changes: 154 additions & 0 deletions .github/workflows/agent-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
name: Agent E2E

# Runs the agent e2e suites (e2e/) against the Conductor OSS server boot JAR
# (Maven Central — it has no GitHub release assets), which ships the
# /agent/* control plane + TaskDef.runtimeMetadata persistence (conductor-oss
# PR #1255) and the agent runtime on by default from 3.32.0-rc.8 onward.
# The Agentspan server JAR is no longer used as a server flavor here; the
# Agentspan CLI binary is still downloaded (its own separate pin) since the
# CLI/skill e2e suites still drive it against this same server.
# Port of the Python SDK's proven agent-e2e workflow; JS deltas only
# (Node toolchain, jest runner; Python kept solely for mcp-testkit).
#
# 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_CLI_VERSION: "0.4.4" # pinned CLI release — independent of the server
CONDUCTOR_OSS_VERSION: "3.32.0-rc.8" # pinned conductor-oss 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 Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Set up Python (mcp-testkit only)
uses: actions/setup-python@v5
with:
python-version: '3.12'
# no `cache: pip` — it requires a requirements.txt/pyproject.toml
# to key on, and this repo has neither (Python is mcp-testkit only)

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

- name: Cache conductor-oss server JAR
id: conductor_oss_jar_cache
uses: actions/cache@v4
with:
path: conductor-server-boot.jar
key: conductor-server-${{ env.CONDUCTOR_OSS_VERSION }}

- name: Download conductor-oss server JAR from Maven Central
if: steps.conductor_oss_jar_cache.outputs.cache-hit != 'true'
run: |
curl -sfL "https://repo1.maven.org/maven2/org/conductoross/conductor-server/${CONDUCTOR_OSS_VERSION}/conductor-server-${CONDUCTOR_OSS_VERSION}-boot.jar" \
--output conductor-server-boot.jar

# Agentspan CLI (CLI/skill suites) — kept on its own separate pin,
# independent of which server JAR is under test.
- name: Download CLI binary from release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "v${AGENTSPAN_CLI_VERSION}" --repo agentspan-ai/agentspan \
--pattern "agentspan_linux_amd64" --output agentspan
chmod +x agentspan

- name: Install SDK deps and build
run: |
npm ci
npm run build

- name: Install mcp-testkit
run: |
python -m pip install --upgrade pip
pip install mcp-testkit

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

- name: Start server
run: |
java -jar conductor-server-boot.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
npm run test:agent-e2e

# The TS suites fail hard when the server is down (no session-skip),
# so this guard is defense-in-depth against a future gate regression
# or a secrets-less fork run skipping everything.
- 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: |
npx tsx e2e/generate-report.ts 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,7 @@ fabric.properties
.env.development.local

/.idea

# jest-junit output (unit CI uses reports/, agent e2e uses results/)
reports/
results/
44 changes: 43 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,51 @@ src/open-api/ # OpenAPI layer
types.ts # Extended types - add custom fields here
src/integration-tests/ # E2E tests against real Conductor server
utils/ # waitForWorkflowStatus, executeWorkflowWithRetry, etc.
src/agents/ # Durable agent layer (merged Agentspan TS SDK)
index.ts # Agent, AgentRuntime, tool, guardrails, handoffs, ...
frameworks/ # LangGraph/LangChain/generic serializers + detection
testing/ # Agent testing toolkit (/agents/testing subpath)
wrappers/ # Vercel AI / LangGraph / LangChain drop-in wrappers
__tests__/ # Colocated jest unit tests (picked up by test:unit)
e2e/ # Agent e2e suites vs live agentspan server (jest.e2e.config.mjs)
cli-bin/ # agentspan CLI helper scripts (Go CLI walk-up probe target)
examples/agents/ # Agent examples (own tsconfig; run via npx tsx)
docs/agents/ # Agent layer documentation
```

### The agent layer (`src/agents/`)

- **Subpath-only exports**: everything agent-flavored ships via `./agents`,
`./agents/testing`, `./agents/vercel-ai`, `./agents/langgraph`,
`./agents/langchain` (see `exports` in package.json + `tsup.config.ts`).
**Never re-export agent symbols from the root** — the agent layer has an
`Action` class that collides with the OpenAPI-generated `Action` type, and
the root re-exports the whole generated surface, which changes with the
server spec.
- Source keeps upstream's ESM-style `.js`-suffixed relative imports (resolved
by `nodenext` for tsc and a `moduleNameMapper` suffix-strip for jest).
- The only coupling to the workflow layer is in `agent-client.ts` and
`worker.ts` (imports from `../sdk` and `../open-api`) — keep it that way.
- Unit tests are colocated in `src/agents/__tests__/` so the existing
`test:unit` glob and per-PR CI matrix pick them up with zero workflow
changes. Agent e2e lives in repo-root `e2e/` and runs only via
`npm run test:agent-e2e` (its own `.github/workflows/agent-e2e.yml` boots a
pinned release server JAR; needs `OPENAI_API_KEY`/`ANTHROPIC_API_KEY`
secrets).
- Agent examples resolve the package name straight to `src/agents` sources via
`examples/agents/tsconfig.json` paths: `npx tsx examples/agents/<file>.ts`.
Framework subdirs (adk/, langgraph/, openai/, vercel-ai/) install their own
deps (`scripts/install-example-deps.sh`); `examples/agents` is excluded from
the root tsconfig.
- `AGENTSPAN_*` env vars (`AGENTSPAN_SERVER_URL`, default
`http://localhost:8080/api`) are the agent layer's config surface — kept
working as-is; `CONDUCTOR_*` aliases are a possible follow-up.

## Commands

```bash
npm test # Unit tests (482+ tests)
npm run build # tsup (ESM + CJS dual output)
npm run build # tsup (root + 5 agent entries, ESM + CJS) + verify:dist
npm run lint # ESLint
npm run generate-openapi-layer # Regenerate from OpenAPI spec

Expand All @@ -55,6 +93,10 @@ CONDUCTOR_SERVER_URL=http://localhost:8080 \
CONDUCTOR_AUTH_KEY=key CONDUCTOR_AUTH_SECRET=secret \
ORKES_BACKEND_VERSION=5 \
npm run test:integration:orkes-v5

# Agent e2e (requires a running agentspan server + LLM keys; CI does this
# against the pinned release JAR — see .github/workflows/agent-e2e.yml)
AGENTSPAN_SERVER_URL=http://localhost:8080/api npm run test:agent-e2e
```

## Post-Change Verification (Required)
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Durable AI agents** -- the Agentspan TypeScript SDK (`@conductor-oss/conductor-agent-sdk`) is merged into this package as new subpath exports: `@io-orkes/conductor-javascript/agents` (core: `Agent`, `AgentRuntime`, `tool`, guardrails, handoffs, memory, schedules, streaming, HITL), `/agents/testing`, and framework wrappers `/agents/vercel-ai`, `/agents/langgraph`, `/agents/langchain`. The agent clients are also first-class citizens of the root export (`AgentClient`, `WorkflowClient`, `Schedule`, `ScheduleInfo`, schedule errors) — the root surface grows additively (minor). Docs at [docs/agents/](docs/agents/README.md); 60+ examples at [examples/agents/](examples/agents/).
- **Migration for `@conductor-oss/conductor-agent-sdk` users:** install `@io-orkes/conductor-javascript` and rewrite import specifiers -- `@conductor-oss/conductor-agent-sdk` becomes `@io-orkes/conductor-javascript/agents`, and the wrapper subpaths gain the `/agents` prefix (e.g. `.../vercel-ai` becomes `.../agents/vercel-ai`).
- New optional peer dependencies (all lazily resolved, install only what you use): `zod`, `zod-to-json-schema`, `ai`, `@langchain/core`, `@langchain/langgraph`. `dotenv` becomes a runtime dependency (the agent entry points load it at import time; the package `sideEffects` field allowlists `dist/agents/**` so bundlers keep that bootstrap).
- **Agent clients in `sdk/clients`, one client for both planes** -- `AgentClient` is an interface (the `/agent/*` control-plane surface); `OrkesAgentClient` is the implementation, obtained via `runtime.client` or `OrkesClients.getAgentClient()`. Both share a single underlying `ConductorClient` (and its one token mint) across control-plane and worker-plane calls -- no bespoke agent-layer auth/transport code exists. Naming note: `getWorkflowClient()` returns the general-purpose `WorkflowExecutor`; the agent `WorkflowClient` (`OrkesClients.getAgentClient().workflows` / `getAgentWorkflowClient()`) adds the agent-execution 404 fallback and token-usage rollup.
- **`AgentRuntime(configuration?, settings?)`** -- two independent, optional constructor arguments: `configuration` (connection/auth -- `OrkesApiConfig` or a pre-built `ConductorClient`, same env chain as every other Conductor client: `CONDUCTOR_*` -> explicit config -> `AGENTSPAN_*` fallback -> `http://localhost:8080` default) and `settings` (behavior-only `AgentConfigOptions`: worker polling/threads, streaming, liveness -- no connection fields). `AgentConfig` carries only behavior now.
- **Verb contract** -- `serve(...agents, { blocking? })` now deploys (registers the workflow def, same as `deploy`) *and* serves in one call; `{ blocking: false }` returns once deploy + worker registration + polling have started instead of blocking until SIGINT/SIGTERM. `deploy(...agents)` gained a variadic form (`DeploymentInfo[]`) alongside the existing single-agent `deploy(agent, { schedules? })` form. `AgentHandle`/`ClientHandle` gained `stop()`. `wait()` (both handle flavors) now rejects once a deadline passes (`timeoutSeconds`-derived, or 10 min default) with an `AgentAPIError` naming the last known status, instead of polling forever.
- **`RunSettings`** -- `RunOptions.runSettings` (`model`, `temperature`, `maxTokens`, `reasoningEffort`, `thinkingBudgetTokens`) applies per-run LLM overrides to `run`/`start`/`stream` without mutating the agent's own config; unset fields keep the agent's values and overrides don't cascade to sub-agents. `RunOptions.model` is sugar for `runSettings.model`.
- **Credentials via `Task.runtimeMetadata`, fail-closed** -- the server resolves declared credentials at poll time and delivers them wire-only on the polled task's `runtimeMetadata`; the SDK injects them into the worker's `process.env` for the call (mutate-invoke-restore) and never fetches or persists them separately. A tool whose declared credential wasn't delivered fails non-retryably naming the missing name -- there is no ambient-env fallback. Requires a server that persists `TaskDef.runtimeMetadata` and delivers `Task.runtimeMetadata` (conductor-oss PR #1255 / agentspan server > 0.4.2); the CI matrix now runs the full agent e2e suite against both `agentspan-server` and the mainline `conductor-oss` boot jar.
- **Liveness monitoring** -- for a stateful (domain-routed) run, a new liveness monitor polls the execution's workflow every `livenessCheckIntervalSeconds` (`AgentConfigOptions.livenessEnabled`, default `true`); a `SCHEDULED`/`IN_PROGRESS` task in that run's domain unpolled (`pollCount === 0`) past `livenessStallSeconds` (default 30s) rejects a blocking `wait()` with the new `WorkerStallError` instead of hanging forever. Framework-spawned agents (no per-run domain) are unaffected.
- **Swarm hand-off contract** -- the per-tool `{source}_transfer_to_{target}` no-op worker now accepts an optional `message` string and echoes it back; the `{agent}_check_transfer` worker adds `transfer_message` (the winning transfer call's message) and, when an LLM turn emits more than one transfer call, `dropped_transfers` (with a warning identifying the honored vs. dropped targets) so a fan-out intent is never silently collapsed to one transfer.
- **`SchedulerClient` pause/resume works on both server families** -- per-schedule pause/resume verbs differ by Conductor family (OSS/embedded map them PUT-only, Orkes GET-only). `pauseSchedule`/`resumeSchedule` now issue PUT first and retry via GET only on HTTP 405 (stateless per call; the new optional `reason` on pause survives the fallback). Previously the GET-only calls failed against OSS/embedded servers. Admin bulk endpoints are unchanged.
- **`SchedulerClient` typed agent-schedule lifecycle** -- absorbed from the deleted agent `ScheduleClient` (below): `save`/`get`/`listForAgent`/`pause`/`resume`/`delete`/`runNow`/`previewNext`/`reconcile` operating on `Schedule`/`ScheduleInfo` with `${agent}-${name}` wire-name prefixing and typed errors (`ScheduleNotFound`, `ScheduleNameConflict`, `InvalidCronExpression`). The constructor also accepts `PromiseLike<Client>` for callers with async client construction.
- **Canonical metrics** -- opt-in harmonized metric surface via `WORKER_CANONICAL_METRICS=true`. See [METRICS.md](METRICS.md) for the full catalog, configuration, and migration guide.
- Bounded `uri` label on `http_api_client_request_seconds`: canonical mode uses path templates (e.g. `/workflow/{workflowId}`) instead of fully-resolved paths, preventing metric cardinality explosion from dynamic IDs.
- `TaskPaused` event type and `PollerOptions.onPaused` callback: emitted when a poll cycle is skipped because the worker is paused. Canonical mode records `task_paused_total`; legacy mode does not (see Implementation Notes in METRICS.md).
- `measurePayloadSize` option in `MetricsCollectorConfig`: controls whether `workflow_input_size_bytes` is recorded via `JSON.stringify` on each `startWorkflow` call. Defaults to `true` for canonical, `false` for legacy.
- `retryServerErrors` option in `OrkesApiConfig` / `RetryFetchOptions` and `CONDUCTOR_RETRY_SERVER_ERRORS` env var: opt-in retry of HTTP 502/503/504 for idempotent methods (GET, HEAD, OPTIONS, PUT, DELETE). Default `false`; set to `true` to enable.
- `WorkflowStatusProbe` in harness: opt-in probe (via `HARNESS_PROBE_RATE_PER_SEC`) that exercises UUID-bearing endpoints to validate template URI metrics.
- `WORKER_LEGACY_METRICS` is reserved for future use. Once canonical metrics become the default, setting `WORKER_LEGACY_METRICS=true` will re-activate the legacy surface. It is not read by the current implementation.
- `resolveOrkesConfig` (used by `createConductorClient`, and so every `OrkesApiConfig`-based client) gains an `AGENTSPAN_*` fallback tier for server URL and auth: `CONDUCTOR_SERVER_URL` -> explicit config -> `AGENTSPAN_SERVER_URL` -> default `http://localhost:8080` (previously an unset server URL threw; it now defaults instead). Auth follows the same shape: `CONDUCTOR_AUTH_KEY`/`SECRET` -> explicit config -> `AGENTSPAN_AUTH_KEY`/`SECRET`.
- `createConductorClient()`'s returned client gains `getAuthenticationHeaders()` (the same `X-Authorization` header the standard call path attaches, for transports that can't go through `client.request`, e.g. SSE -- borrows the client's TTL-aware token, mints/caches nothing of its own) and `stopBackgroundRefresh()` (stops the client's background token-refresh interval; previously that handle was captured locally and dropped, leaking the interval for the life of the process).
- Logger env chain: `DefaultLogger` now also reads `AGENTSPAN_LOG_LEVEL` as a fallback when `CONDUCTOR_LOG_LEVEL` is unset.

### Changed

Expand All @@ -24,6 +39,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `http_api_client_request` timing is now recorded automatically by `wrapFetchWithRetry` when a metrics collector is active (via `createMetricsCollector()` or `setHttpMetricsObserver`), covering both successful responses and network-error fallback paths. A lightweight request interceptor captures OpenAPI path templates so the canonical `uri` label uses bounded-cardinality templates in all cases. Previously, `recordApiRequestTime` existed but was not wired into the HTTP pipeline -- [details](METRICS.md#implementation-notes).
- Added optional `durationMs` field to `TaskUpdateFailure` event, recording the duration of the last update attempt. Declared optional so existing event listener implementations are unaffected.

### Removed

- **Agent `ScheduleClient` and `SchedulerFetcher`** (agents subpath; never released, no backward compatibility) -- schedules now ride the SDK `SchedulerClient`, re-exported from `@io-orkes/conductor-javascript/agents`. Method names and signatures are unchanged; migration is the import/type rename only (`ScheduleClient` -> `SchedulerClient`). The `schedules.*` namespace, `runtime.schedulesClient()`, `deploy({ schedules })` and `AgentClient.schedule()` are untouched.

### Deprecated

- Legacy metric names remain the default during the transition period. Migration guidance is in [METRICS.md](METRICS.md#migrating-from-legacy-to-canonical).
Loading
Loading