Skip to content

Commit 891bd06

Browse files
kowser-orkesKowserclaude
authored
Merge Agentspan SDK and Implement following the Implementation Guide (#131)
* Add durable-agent layer at src/agents exposed via /agents subpath exports Merge the Agentspan TypeScript SDK source (@conductor-oss/conductor-agent-sdk 0.3.0, agentspan@f8704fa3) into this package as src/agents/, published only via subpath exports: ./agents, ./agents/testing, ./agents/vercel-ai, ./agents/langgraph, ./agents/langchain. The root export surface is byte-identical to before (verified via compiler API), so the one name collision (Action) never materializes. Changes beyond the verbatim copy: - agent-client.ts / worker.ts: import from ../sdk and ../open-api instead of the package name (the only coupling to the workflow layer) - tool.ts: rename the createRequire binding to peerRequire (top-level `require` is reserved in this repo's CJS module flavor) - lint conformance to this repo's strict+stylistic ruleset (~41 fixes: no-op arrows, guard-throws replacing non-null assertions, Reflect.deleteProperty, for-of); no-explicit-any and no-unsafe-function-type stay at upstream's own "warn" contract via a scoped eslint override - tsup.config.ts replaces the inline package.json block: 6 entries, esm+cjs+dts; optional peers external; verify:dist gate loads every exports subpath in both formats - package.json: dotenv to dependencies (import-time side effect in config.ts), 5 optional peerDependencies, sideEffects allowlists dist/agents/**, tsx/zod/zod-to-json-schema devDeps - tsconfig: lib es2022 -> es2023 (stream.ts uses Array.findLast; superset, Node >= 18 supports it), package-name paths for in-repo resolution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Port agent unit tests to jest (colocated __tests__) and migrate cli-bin 38 of upstream's 41 unit/cli-bin suites move to src/agents/__tests__/, where the existing test:unit glob and per-PR CI matrix pick them up with zero workflow changes. Test-count parity with upstream vitest is exact: 761 = 860 - 99 in the three deferred files. Deferred: - kitchen-sink-structural.test.ts (imports examples/kitchen-sink; lands with the examples in the next commit) - validation/{algorithmic,event-audit}.test.ts (they test the validation/ LLM-judge harness, whose migration is explicitly deferred) Port mechanics: vitest imports -> @jest/globals, vi.* -> jest.*, toBeTypeOf/toHaveBeenCalledOnce -> jest equivalents, vi.stubGlobal -> local helpers/stub-global.ts (upstream never unstubs; per-file isolation contains it), relative ../../src paths -> colocated paths, skills fixtures colocated under __tests__/fixtures/. The two jest.mock("child_process") factories hoist correctly under ts-jest (verified green). cli-bin/ lands at repo root (the Go CLI walk-up probe expects <dir>/cli-bin) with imports retargeted to src/agents/. deploy.ts gets a type-only fix for its workflowName read (upstream never typechecked cli-bin; runtime shape unchanged). jest.config.mjs: package-name mappers to in-repo sources plus the standard ESM .js-suffix strip for relative imports. eslint: upstream's warn contract extended to cli-bin; stylistic-only relaxations scoped to the migrated tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Migrate agent examples and docs under examples/agents and docs/agents 221 example files (top-level + quickstart + adk/langgraph/openai/vercel-ai framework subdirs) land at examples/agents/, the 6 agent docs at docs/agents/ (sidestepping the README and api-reference path collisions). All imports and install instructions rewritten from @conductor-oss/conductor-agent-sdk to @io-orkes/conductor-javascript (/agents subpaths), including the runtime error-message strings in src/agents wrappers — zero old-name references remain outside git history. Run story: top-level/quickstart examples resolve the package name straight to src/agents via examples/agents/tsconfig.json paths (npx tsx, no install); framework subdirs install their own deps (scripts/install-example-deps.sh, nonexistent langchain/ entry dropped) and gain an explicit file:../../.. SDK dep; langgraph/ also gains the @langchain deps it silently inherited from upstream's deleted examples workspace manifest. Also lands the deferred kitchen-sink-structural.test.ts (66 tests; unit suite now 1500 green) and a type-only fix upstream never caught because examples were never typechecked: schedules-api.ts runNow's first overload makes opts optional, matching the documented 1-arg default usage. Root tsconfig excludes examples/agents (framework subdir deps are not installed at the root); eslint gains a scoped override for the four rules the didactic files break en masse — upstream ignored examples/ in lint entirely. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Port agent e2e suites to jest under repo-root e2e/ All 25 upstream e2e suites move to e2e/ (with fixtures/, _configs/, and the loose harness scripts under e2e/tools/), run by jest.e2e.config.mjs: testMatch e2e/**, 60s default timeout, maxWorkers 3 (upstream's maxForks 3), jest-junit to results/junit-e2e.xml. New script: npm run test:agent-e2e (--forceExit, matching the repo's other jest scripts — agent polling keeps handles open after LLM suites). Deliberately invisible to test/test:unit/test:integration globs — per-PR CI cost is unchanged (verified: unit run still 74 suites). Beyond the mechanical vitest-to-jest pass, six vitest-isms needed real porting (verified by loading all 25 suites under jest — 196 tests enumerate clean — and by running suites 1/2/9 against a live release-JAR server): - expect(actual, message) two-arg form (265 sites, jest 30 rejects it) -> expectMsg helper in e2e/helpers.ts that prepends the diagnostic message to matcher failures - describe(name, { timeout }, fn) options object (19 files) -> file-level jest.setTimeout - it.skipIf(cond) (19 sites) -> itSkipIf helper - it.fails -> it.failing (suite 7) - it(name, { retry: 2 }, fn) (4 sites) -> file-level jest.retryTimes(2) (jest has no per-test retry; both files are LLM-variability suites) - top-level await gates (suites 11/21/23; illegal in CJS) -> synchronous require for the langgraph availability probe, and fail-hard beforeAll health/scheduler gates (matches the TS-e2e fail-hard philosophy; the CI workflow health-gates the server before tests run) Live-server proof (local V1): suites 1/2/9 against agentspan-server 0.4.0 — 11 tests pass; the only failures are server-side OpenAI 401s from the missing local LLM key (the CI run with repo secrets is the full proof). Unused-var noise (port artifacts + upstream) fixed with _-prefixes and import trims; the stylistic eslint relaxations for migrated tests extend to e2e/**. jest-junit output dirs (reports/, results/) are now gitignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add per-PR agent-e2e workflow (release JAR + CLI + mcp-testkit) Port of the Python SDK's implemented agent-e2e workflow. Kept verbatim: pull_request + workflow_dispatch triggers, per-ref concurrency with cancel-in-progress, pinned AGENTSPAN_VERSION 0.4.0 (server JAR from the GitHub release, cached by version; linux CLI binary), temurin 21, hard 90s health gate that dumps server.log on failure, executed>0 junit guard (defense-in-depth here — the TS suites fail hard when the server is down), junit + HTML + server.log artifacts at 14-day retention, 45-min timeout, job-level OPENAI_API_KEY/ANTHROPIC_API_KEY secrets (must be added to this repo's settings before the first live run). JS deltas: Node 22 + npm ci + npm run build + npm run test:agent-e2e as the test step; Python setup retained only for mcp-testkit; HTML report via npx tsx e2e/generate-report.ts; no CONDUCTOR_MP_START_METHOD (Python multiprocessing concern, no JS analog). Rehearsed locally: actionlint clean; health gate passed against the booted 0.4.0 JAR; guard correctly fails an all-skipped junit (0/196) and the report generator renders the real junit output. The test:agent-e2e script pins jest-junit's output to results/junit-e2e.xml via env vars — the package.json jest-junit block (reports/jest-junit.xml) outranks reporter options, and env vars outrank the block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Document the merged durable-agent layer README: new Durable AI Agents section (subpath quickstart mirroring examples/agents/quickstart/01-basic-agent.ts, wrapper/testing subpaths, docs + examples pointers) and a Documentation-table row. AGENTS.md: src/agents feature area — repo layout additions (src/agents, e2e/, cli-bin/, examples/agents, docs/agents), the subpath-only export rule (root re-export ban and the OpenAPI Action collision that motivates it), colocated-test and e2e conventions, example run story, AGENTSPAN_* config surface, and the agent-e2e command. CHANGELOG: additive-minor entry with a migration note for @conductor-oss/conductor-agent-sdk users (specifier rewrite, /agents-prefixed wrapper subpaths, optional peers, dotenv/sideEffects behavior). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ci): drop pip cache from agent-e2e setup-python actions/setup-python's `cache: pip` hard-fails when the repo has no requirements.txt or pyproject.toml to hash for the cache key — true for this repo, where Python exists only to run mcp-testkit. Carried over from the Python SDK's agent-e2e workflow, where a pyproject.toml exists. First observed on PR #131: the job died in "Set up Python" before any test ran, and the silently-empty guard correctly reported 0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Switch port to 8080 * fix(e2e): restore diag/codeOutputs names over-prefixed in suite 10 port The Stage-4 lint pass blanket-prefixed suite 10's diag and codeOutputs locals as unused, but most scopes still reference the bare names inside expectMsg messages and output assertions — ReferenceError at runtime. ts-jest is transpile-only, so it surfaced only in the first secrets- enabled CI run (6 failures, the run's only red). Only genuinely-unused declarations keep the underscore (upstream parity; upstream never linted e2e). Verified live: suite 10 vs local server JAR 0.4.0 with real keys — 8 passed, 1 skipped (Jupyter, needs kernel). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(scheduler): PUT-first pause/resume with 405→GET fallback + typed agent lifecycle surface Per-schedule pause/resume verbs differ by server family: OSS/embedded Conductor maps them PUT-only, Orkes Conductor GET-only. SchedulerClient now issues PUT first and retries via GET only on HTTP 405, stateless per call, with the pause reason re-applied on the fallback URL (mirrors the Python SDK's OrkesSchedulerClient). Implemented in the hand-written wrapper via raw non-throwing client calls — the generated transport is untouched and regeneration stays safe. SchedulerClient also absorbs the typed agent-schedule lifecycle surface (save/get/listForAgent/pause/resume/delete/runNow/previewNext/reconcile) operating on Schedule/ScheduleInfo with wire-name prefixing and typed errors via _translate. The models/mapping/errors relocate verbatim from src/agents/schedule.ts to src/sdk/clients/agent/schedule.ts (Python parity: conductor/client/ai/schedule.py); the agents module re-exports them. Ctor widened to Client | PromiseLike<Client> so callers with async client construction keep synchronous accessors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agents)!: delete ScheduleClient/SchedulerFetcher — schedules ride SchedulerClient BREAKING CHANGE (agent layer, no backward compatibility): the raw-fetch ScheduleClient class and SchedulerFetcher interface are gone from the ./agents surface. SchedulerClient — which absorbed the same typed lifecycle methods (save/get/listForAgent/pause/resume/delete/runNow/ previewNext/reconcile) in the previous commit — is re-exported from ./agents in their place; every method call site keeps its name and signature. The scheduling e2e suite needed exactly two lines (import + one type annotation); the schedules.* module facade, runtime schedulesClient(), deploy({schedules}) reconciliation and AgentClient.schedule() are unchanged. AgentClient.schedules now hands its memoized Conductor-client promise to SchedulerClient (single transport; the SchedulerFetcher raw-fetch path is deleted, _rawRequestUntyped remains for /agent/* control-plane calls). An absence test pins the deletion, mirroring the Python SDK's test_agent_schedule_client_is_gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sdk): AgentClient + agent WorkflowClient live in sdk/clients, exposed via OrkesClients Moves the agent control-plane client and the agent-flavored workflow client from src/agents/ to src/sdk/clients/agent/ (names kept — nothing on the root surface collides: Conductor's workflow client is WorkflowExecutor). OrkesClients gains getAgentClient() and getAgentWorkflowClient(); AgentClient's constructor accepts an optional pre-built client ((AgentConfigOptions & {client?}) | AgentConfig) which pre-seeds the memoized promise, so the factory path reuses one Conductor client across workflow + schedule surfaces. The /agent/* raw transport and JWT mint path are unchanged; decodeJwtExp stays exported. Cycle guard: the moved files import createConductorClient and the agent domain modules via deep paths, never the sdk barrel; madge reports no new cycles (the AgentClient↔WorkflowClient type-only edge predates the move). The ./agents subpath re-exports everything from the new homes — its surface is unchanged. The enriched ConductorClient type is not re-exported at root (the deprecated bare alias keeps that name). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: single-scheduler consolidation + agent clients in sdk/clients CHANGELOG (Unreleased): OrkesClients agent getters, PUT-first/405→GET pause-resume verb handling, absorbed typed schedule lifecycle, and the ScheduleClient/SchedulerFetcher removal with its one-line migration. Agent docs re-point the schedule surface at SchedulerClient and note the WorkflowExecutor vs agent WorkflowClient distinction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(sdk)!: AgentClient interface + OrkesAgentClient on the shared client's request pipeline; SSE auth borrowing (spec R1/R2) Fork point: feature/combine-agentspan @ a410bad. Executes idea-12/design-typescript.md DD2-DD4 (analysis-typescript.md T1/T6/T7/T10/T13). - handleAuth gains getToken() (TTL-aware, reuses the existing guarded refresh); createConductorClient exposes getAuthenticationHeaders()/stopBackgroundRefresh() on the returned client so /agent/* can borrow the shared client's token instead of minting a second one (R2). - Split AgentClient into an interface (11 spec ops: startAgent, deployAgent, compile, status, getExecution, listExecutions, respond, stop, signal, stream, close) and a new OrkesAgentClient implementation. Every non-streaming call rides client.request(...) with one error choke point (404 -> AgentNotFoundError, else -> AgentAPIError), killing the raw-fetch transport and the T10 plain-Error leak. Deleted _token/_tokenExp/_mintPromise/_authHeaders/_mintToken/decodeJwtExp and the raw _request/_rawRequestUntyped methods. An explicit config.apiKey (already-minted token) still wins verbatim, wired onto both the SSE header provider and the shared client's own auth so REST calls carry it too. - AgentStream takes a header provider (() => Promise<Record<string,string>>) resolved fresh per (re)connect instead of a static snapshot (T7); accepts an initial lastEventId; gained close() and a dedicated SSEUnavailableError for non-2xx initial-connect rejection (falls straight to the polling fallback). - OrkesClients.getAgentClient() returns the AgentClient interface type; AgentRuntime.client is now an OrkesAgentClient built on the same transport. - Rewrote the tests that pinned the old internals: agent-client-auth.test.ts (now the R2 spec guard: one mint across N /agent/* calls, anonymous path sends no header), the runtime.test.ts _httpRequest/SSE/stream/credentials suites (mock the generated OpenAPI transport instead of global.fetch directly), OrkesClients.agent.test.ts and suite23 (ctor rename). Not yet done (S2+): AgentConfig slimming, worker-plane transport reroute, CONDUCTOR_*->AGENTSPAN_* env fallback, RunSettings, runtimeMetadata credentials, liveness, swarm transfer contract. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(agents)!: AgentRuntime(configuration, settings); behavior-only AgentConfig; one client for control+worker planes; CONDUCTOR_*->AGENTSPAN_* env chain (spec R3/R4/R5) Executes idea-12/design-typescript.md DD5-DD7 (analysis-typescript.md T3/T4/T11). - AgentRuntime ctor becomes (configuration?: OrkesApiConfig | ConductorClient, settings?: AgentConfigOptions) - connection/auth split from behavior knobs, no shim (agent layer unreleased; all 232 example constructions are zero-arg). Builds ONE shared ConductorClient (or adopts an injected one), wraps it in OrkesAgentClient. runtime.client narrows to the AgentClient interface (spec R1 surface); a private _agentClient reference carries the full surface (workflows/schedules/request escape-hatch) for the runtime's internal use. - OrkesAgentClient's own ctor slims to (configuration?: OrkesApiConfig | ConductorClient) - no more embedded AgentConfig/apiKey passthrough (moved fully to the shared client's keyId/keySecret model). Exposes apiBaseUrl()/apiBaseUrlSync() so callers building SSE URLs don't need config.serverUrl (which no longer exists). - AgentConfig slimmed to 7 behavior-only knobs: workerPollIntervalMs, workerThreadCount (renamed from workerThreads, now wired to ConductorWorker.concurrency instead of a hardcoded 1), autoStartWorkers (gates startPolling() in run/start; serve always starts), streamingEnabled (wired via AgentStream's new skipSse flag -> straight to the polling fallback), livenessEnabled/livenessStallSeconds/livenessCheckIntervalSeconds (config lands now; S5 adds the reader). Deleted: serverUrl, apiKey, authKey, authSecret, logLevel, llmRetryCount, credentialStrictMode, autoStartServer, daemonWorkers, normalizeServerUrl. - WorkerManager takes a getClient() resolver instead of building its own client: deleted the header-injecting custom fetch, the headersProvider plumbing, and the process.env.CONDUCTOR_SERVER_URL clobber (T4). Retains serverUrl/headers fields (now derived from the shared client) only for the pre-S4 resolveCredentials()/runWithCredentialContext() pull path. - resolveOrkesConfig gains the R3 env chain: host CONDUCTOR_SERVER_URL -> explicit config -> AGENTSPAN_SERVER_URL -> localhost:8080 default; auth CONDUCTOR_AUTH_KEY/SECRET -> explicit -> AGENTSPAN_AUTH_KEY/SECRET. The unconditional console.warn for missing CONDUCTOR_AUTH_KEY/SECRET now routes through the configured (or default) logger at debug. DefaultLogger's default level gains the same env chain (CONDUCTOR_LOG_LEVEL -> AGENTSPAN_LOG_LEVEL -> INFO). - Rewrote the tests that pinned the deleted fields or the old transport: config.test.ts (7-knob set + a type-level guard that AgentConfigOptions carries no connection/auth/log keys), worker.test.ts (shared-client getClient() resolver + concurrency wiring), runtime.test.ts (ctor split + keyId/keySecret in place of apiKey/authKey), resolveOrkesConfig.test.ts + createConductorClient.test.ts (new fallback/default coverage), OrkesClients.agent.test.ts and suite23 (cast to OrkesAgentClient where the narrow interface no longer exposes workflows/schedules/getClient). Not yet done (S3+): verb contract (serve=deploy+serve, variadic deploy, handle stop), RunSettings, runtimeMetadata credentials, liveness reader, swarm transfer contract. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * feat(agents): serve=deploy+serve with blocking option; variadic deploy; handle stop; RunSettings per-run overrides (spec R8/R9) - serve(...agents, {blocking?}) deploys each agent (native + framework) before registering/starting its workers; blocking:false returns after deploy + registration + polling start instead of waiting for SIGINT/SIGTERM. Trailing-arg detection reuses detectFramework + a keys-subset check so a real Agent/framework object is never misclassified as options. - deploy() gains a variadic overload (deploy(...agents) -> DeploymentInfo[]) alongside the existing deploy(agent, opts?) form; both share a new _deployViaServer helper (also used by serve). - AgentHandle (native + framework) gains stop(): client.stop() + best-effort client.signal(id, "stopped") to unblock any pending wait(), mirroring ClientHandle's existing routing. - New RunSettings type (model/temperature/maxTokens/reasoningEffort/ thinkingBudgetTokens) applied to the serialized agentConfig immediately before startAgent in run()/start() (stream() delegates to start()); null-check gating so zero values (temperature: 0) still apply; unknown keys throw. RunOptions.model becomes sugar for runSettings.model on all paths (native + framework); an explicit runSettings.model wins. Fork point a410bad on feature/combine-agentspan; design DD9/DD10 (idea-12/design-typescript.md). * feat(agents)!: worker credentials via Task.runtimeMetadata, fail-closed; retire /workers/secrets; agent-e2e two-server matrix (spec R6/R12) - Dispatch (worker.ts _wrapWorker): replace execution-token extraction + resolveCredentials POST with a read of the polled task's runtimeMetadata (typed local extension, no OpenAPI regen). Any declared credential name missing from the delivered map -> NonRetryableException naming the missing names and the required server capability. Ambient process.env is never read as a fallback. injectSecretsForInvocation's mutex-protected env injection is unchanged -- only the source of resolved values changed. - Deletions (R12): resolveCredentials, extractExecutionToken, all __agentspan_ctx__/executionToken handling, /workers/secrets references. CredentialContext now holds resolved name->value pairs directly; getCredential()/runWithCredentialContext()/setCredentialContext() keep their conceptual role but take the delivered map, not a token to resolve. WorkerManager's now-dead _serverUrl/headers fields (kept from S2 for the pull path) are removed. - e2e/helpers.ts gains two session-scoped capability probes: checkRuntimeMetadataCapability() (register/read-back a scratch TaskDef) and checkSecretWriteCapability() (probe PUT /secrets) -- both needed because a standalone conductor-oss server's secret store is env-backed and read-only. Wired into test_suite2 (full skip) and test_suite4/5 (Phase 2 skip) so credential-lifecycle assertions degrade gracefully instead of failing hard on an incapable server. - .github/workflows/agent-e2e.yml: matrix over server flavor (fail-fast off) -- Lane A (agentspan, bumped 0.4.0 -> 0.4.4) kept verbatim; Lane B (conductor-oss v3.32.0-rc.8, boot jar from Maven Central -- no GitHub release assets) added, verified locally (health shape, /api/agent/* plane, runtimeMetadata round-trip, and a real LLM-only run all confirmed live against the downloaded jar). No LLM-integration registration step needed -- both servers read OPENAI_API_KEY/ANTHROPIC_API_KEY directly from the server process's own environment. Deviation from the plan's file list (necessary consequence of the R12 deletion, not scope creep): examples/16h-credentials-external-worker.ts imported the two deleted functions directly and is rewritten to read runtimeMetadata off the polled task instead; examples/16-credentials- isolated-tool.ts and serializer.ts had comments describing the retired execution-token mechanism, now corrected. test_suite1's plan-compile test had the same stale comment, also fixed. Fork point a410bad on feature/combine-agentspan; design DD8 (idea-12/design-typescript.md). * feat(agents): liveness monitor + wait deadline for stateful runs; swarm transfer hand-off contract (spec R11/R13) R11 — liveness: new src/agents/liveness.ts LivenessMonitor polls the workflow every livenessCheckIntervalSeconds (config knobs already landed in S2) for a stateful (domain-routed) run; a SCHEDULED/IN_PROGRESS task in that domain with pollCount=0 for longer than livenessStallSeconds fires a new WorkerStallError (errors.ts), rejecting a blocking wait() instead of hanging forever. Only wired for native start() — framework agents never route through a domain, so there's nothing to watch. Monitor self-stops on terminal workflow status or handle disposal (wait()/stop()). wait() deadline (T9 sibling): both the native and framework handle's wait() now cap at timeoutSeconds*1000+30s (or a 600s default), throwing AgentAPIError naming the last status — mirrors OrkesAgentClient's existing ClientHandle.wait. R13 — swarm transfer contract: the per-tool transfer no-op worker now accepts an optional `message` and echoes {message} back when non-empty ({} otherwise, backward compatible). The {agent}_check_transfer worker adds `transfer_message` (the winning call's inputParameters.message, tolerating an older `arguments` key variant, stringified, "" when absent) and `dropped_transfers` for non-winning transfer calls when there is more than one, with a warning naming the honored and dropped targets — a fan-out intent is never silently dropped. Tests: new liveness.test.ts unit-tests LivenessMonitor directly (domain gating, dedup, terminal auto-stop, error-swallowing) with fake timers; runtime.test.ts adds wiring tests (stateful-only, livenessEnabled:false, end-to-end stall -> WorkerStallError, deadline expiry) plus a local afterEach(jest.restoreAllMocks()) since LivenessMonitor.prototype spies (unlike this file's per-instance spies) leak across tests without it; swarm-workers.test.ts extends check_transfer/transfer-worker coverage for the new output shape. Validation: tsc shows only the pre-existing Mock<UnknownFunction> test-mock typing pattern growing by the same handful of new call sites (no new category); lint 0 errors; full unit suite 1566/1566; build clean (6 dist entrypoints). * docs(agents): CHANGELOG final surface; serve contract example prose; credential + liveness docs CHANGELOG.md [Unreleased] rewritten to the final agent-layer shape (no migration-shim framing -- the layer is still unreleased): AgentClient interface via OrkesClients.getAgentClient()/runtime.client sharing one Conductor client; AgentRuntime(configuration, settings) ctor; behavior-only AgentConfig; verb contract (serve = deploy + serve, blocking, variadic deploy, handle stop, deadline-aware wait()); RunSettings; runtimeMetadata credentials (fail-closed, server-capability note); liveness monitoring; swarm hand-off contract. Released-surface additive lines called out separately: resolveOrkesConfig's AGENTSPAN_* fallback + localhost:8080 default, getAuthenticationHeaders/stopBackgroundRefresh, logger env chain. docs/agents/{api-reference,advanced,writing-agents}.md: corrected drift accumulated across Stages 1-5 (AgentClient was documented as a directly instantiable class; AgentConfigOptions still listed deleted connection/dead fields; credentials described as pulled from a secret store rather than delivered wire-only on Task.runtimeMetadata). Added RunSettings and liveness monitoring sections (previously undocumented), WorkerStallError, and the R10 "no per-run mutable capture" invariant for tool() authors. examples/agents/: 63-deploy.ts/63b-serve.ts/63c-run-by-name.ts headers rewritten for the serve-deploys contract (serve() no longer requires a prior deploy() call). Scripted, comment-only sweep across 100 example files' "Production pattern" footer wording to match (zero code churn, verified via git diff --stat). Validation: lint 0 errors (358 pre-existing warnings), tsc baseline-clean (285, unchanged), build clean, unit 1566/1566. * ci(agent-e2e): drop agentspan server lane, run conductor-oss only conductor-oss ships the agent runtime by default from 3.32.0-rc.8, so the dual-lane matrix no longer needs the Agentspan release JAR as a second server flavor. The Agentspan CLI binary stays on its own separate pin (AGENTSPAN_CLI_VERSION) since e2e/helpers.ts's CLI_PATH is still exercised by the CLI/skill suites against this same server. Single lane, single job — matches the shape Python's/Java's/C#'s agent-e2e.yml now use. * refactor(scheduler): borrow raw-call auth from the shared client's accessor Replace the copied X_AUTHORIZATION_SECURITY descriptor with getAuthenticationHeaders() — the same TTL-aware token the generated call path attaches, already exposed for transports outside the generated methods (SSE). One source of auth truth; nothing to drift if the spec's security scheme changes. Mirrors the Python SDK, where auth rides the transport unconditionally and the raw pause/resume fallback needs no per-call metadata. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Kowser <kowser.orkes@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 6450358 commit 891bd06

441 files changed

Lines changed: 76967 additions & 84 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/agent-e2e.yml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
name: Agent E2E
2+
3+
# Runs the agent e2e suites (e2e/) against the Conductor OSS server boot JAR
4+
# (Maven Central — it has no GitHub release assets), which ships the
5+
# /agent/* control plane + TaskDef.runtimeMetadata persistence (conductor-oss
6+
# PR #1255) and the agent runtime on by default from 3.32.0-rc.8 onward.
7+
# The Agentspan server JAR is no longer used as a server flavor here; the
8+
# Agentspan CLI binary is still downloaded (its own separate pin) since the
9+
# CLI/skill e2e suites still drive it against this same server.
10+
# Port of the Python SDK's proven agent-e2e workflow; JS deltas only
11+
# (Node toolchain, jest runner; Python kept solely for mcp-testkit).
12+
#
13+
# These tests call real LLMs via the OPENAI_API_KEY / ANTHROPIC_API_KEY
14+
# repo secrets. Fork PRs cannot see repo secrets, so for them the run
15+
# fails at the silently-empty guard rather than passing vacuously.
16+
17+
on: [pull_request, workflow_dispatch]
18+
19+
concurrency:
20+
group: agent-e2e-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
env:
24+
AGENTSPAN_CLI_VERSION: "0.4.4" # pinned CLI release — independent of the server
25+
CONDUCTOR_OSS_VERSION: "3.32.0-rc.8" # pinned conductor-oss release — bump deliberately
26+
27+
jobs:
28+
agent-e2e:
29+
runs-on: ubuntu-latest
30+
timeout-minutes: 45
31+
env:
32+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
33+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
34+
AGENTSPAN_SERVER_URL: http://localhost:8080/api
35+
AGENTSPAN_CLI_PATH: ${{ github.workspace }}/agentspan
36+
steps:
37+
- name: Checkout code
38+
uses: actions/checkout@v4
39+
40+
- name: Set up Node
41+
uses: actions/setup-node@v4
42+
with:
43+
node-version: 22
44+
cache: npm
45+
46+
- name: Set up Python (mcp-testkit only)
47+
uses: actions/setup-python@v5
48+
with:
49+
python-version: '3.12'
50+
# no `cache: pip` — it requires a requirements.txt/pyproject.toml
51+
# to key on, and this repo has neither (Python is mcp-testkit only)
52+
53+
- name: Set up Java
54+
uses: actions/setup-java@v4
55+
with:
56+
distribution: temurin
57+
java-version: '21'
58+
59+
- name: Cache conductor-oss server JAR
60+
id: conductor_oss_jar_cache
61+
uses: actions/cache@v4
62+
with:
63+
path: conductor-server-boot.jar
64+
key: conductor-server-${{ env.CONDUCTOR_OSS_VERSION }}
65+
66+
- name: Download conductor-oss server JAR from Maven Central
67+
if: steps.conductor_oss_jar_cache.outputs.cache-hit != 'true'
68+
run: |
69+
curl -sfL "https://repo1.maven.org/maven2/org/conductoross/conductor-server/${CONDUCTOR_OSS_VERSION}/conductor-server-${CONDUCTOR_OSS_VERSION}-boot.jar" \
70+
--output conductor-server-boot.jar
71+
72+
# Agentspan CLI (CLI/skill suites) — kept on its own separate pin,
73+
# independent of which server JAR is under test.
74+
- name: Download CLI binary from release
75+
env:
76+
GH_TOKEN: ${{ github.token }}
77+
run: |
78+
gh release download "v${AGENTSPAN_CLI_VERSION}" --repo agentspan-ai/agentspan \
79+
--pattern "agentspan_linux_amd64" --output agentspan
80+
chmod +x agentspan
81+
82+
- name: Install SDK deps and build
83+
run: |
84+
npm ci
85+
npm run build
86+
87+
- name: Install mcp-testkit
88+
run: |
89+
python -m pip install --upgrade pip
90+
pip install mcp-testkit
91+
92+
- name: Start mcp-testkit
93+
run: |
94+
mcp-testkit --transport http --port 3001 &
95+
sleep 2
96+
97+
- name: Start server
98+
run: |
99+
java -jar conductor-server-boot.jar --server.port=8080 > server.log 2>&1 &
100+
101+
- name: Wait for server health
102+
run: |
103+
for i in $(seq 1 45); do
104+
if curl -sf http://localhost:8080/health | grep -q '"healthy"[[:space:]]*:[[:space:]]*true'; then
105+
echo "server healthy after ~$((i*2))s"; exit 0
106+
fi
107+
sleep 2
108+
done
109+
echo "::error::server failed to become healthy within 90s"
110+
tail -100 server.log
111+
exit 1
112+
113+
- name: Run e2e suites
114+
run: |
115+
mkdir -p results
116+
npm run test:agent-e2e
117+
118+
# The TS suites fail hard when the server is down (no session-skip),
119+
# so this guard is defense-in-depth against a future gate regression
120+
# or a secrets-less fork run skipping everything.
121+
- name: Guard against silently-empty runs
122+
if: always()
123+
run: |
124+
python - <<'EOF'
125+
import glob
126+
import sys
127+
import xml.etree.ElementTree as ET
128+
129+
total = executed = 0
130+
for path in glob.glob("results/junit-*.xml"):
131+
root = ET.parse(path).getroot()
132+
for suite in root.iter("testsuite"):
133+
t = int(suite.get("tests", 0))
134+
sk = int(suite.get("skipped", 0))
135+
total += t
136+
executed += t - sk
137+
print(f"executed {executed}/{total} tests")
138+
sys.exit(0 if executed > 0 else 1)
139+
EOF
140+
141+
- name: Generate HTML report
142+
if: always()
143+
run: |
144+
npx tsx e2e/generate-report.ts results/junit-e2e.xml results/report.html || true
145+
146+
- name: Upload results
147+
if: always()
148+
uses: actions/upload-artifact@v4
149+
with:
150+
name: agent-e2e-results
151+
path: |
152+
results/
153+
server.log
154+
retention-days: 14

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,7 @@ fabric.properties
8585
.env.development.local
8686

8787
/.idea
88+
89+
# jest-junit output (unit CI uses reports/, agent e2e uses results/)
90+
reports/
91+
results/

AGENTS.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,51 @@ src/open-api/ # OpenAPI layer
4040
types.ts # Extended types - add custom fields here
4141
src/integration-tests/ # E2E tests against real Conductor server
4242
utils/ # waitForWorkflowStatus, executeWorkflowWithRetry, etc.
43+
src/agents/ # Durable agent layer (merged Agentspan TS SDK)
44+
index.ts # Agent, AgentRuntime, tool, guardrails, handoffs, ...
45+
frameworks/ # LangGraph/LangChain/generic serializers + detection
46+
testing/ # Agent testing toolkit (/agents/testing subpath)
47+
wrappers/ # Vercel AI / LangGraph / LangChain drop-in wrappers
48+
__tests__/ # Colocated jest unit tests (picked up by test:unit)
49+
e2e/ # Agent e2e suites vs live agentspan server (jest.e2e.config.mjs)
50+
cli-bin/ # agentspan CLI helper scripts (Go CLI walk-up probe target)
51+
examples/agents/ # Agent examples (own tsconfig; run via npx tsx)
52+
docs/agents/ # Agent layer documentation
4353
```
4454

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

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

@@ -55,6 +93,10 @@ CONDUCTOR_SERVER_URL=http://localhost:8080 \
5593
CONDUCTOR_AUTH_KEY=key CONDUCTOR_AUTH_SECRET=secret \
5694
ORKES_BACKEND_VERSION=5 \
5795
npm run test:integration:orkes-v5
96+
97+
# Agent e2e (requires a running agentspan server + LLM keys; CI does this
98+
# against the pinned release JAR — see .github/workflows/agent-e2e.yml)
99+
AGENTSPAN_SERVER_URL=http://localhost:8080/api npm run test:agent-e2e
58100
```
59101

60102
## Post-Change Verification (Required)

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **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/).
13+
- **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`).
14+
- 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).
15+
- **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.
16+
- **`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.
17+
- **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.
18+
- **`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`.
19+
- **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.
20+
- **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.
21+
- **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.
22+
- **`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.
23+
- **`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.
1224
- **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.
1325
- 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.
1426
- `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).
1527
- `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.
1628
- `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.
1729
- `WorkflowStatusProbe` in harness: opt-in probe (via `HARNESS_PROBE_RATE_PER_SEC`) that exercises UUID-bearing endpoints to validate template URI metrics.
1830
- `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.
31+
- `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`.
32+
- `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).
33+
- Logger env chain: `DefaultLogger` now also reads `AGENTSPAN_LOG_LEVEL` as a fallback when `CONDUCTOR_LOG_LEVEL` is unset.
1934

2035
### Changed
2136

@@ -24,6 +39,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2439
- `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).
2540
- Added optional `durationMs` field to `TaskUpdateFailure` event, recording the duration of the last update attempt. Declared optional so existing event listener implementations are unaffected.
2641

42+
### Removed
43+
44+
- **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.
45+
2746
### Deprecated
2847

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

0 commit comments

Comments
 (0)