Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a04ddfb
feat(harness): experimental agentic testing proof of concept (untested)
chrishagglund-ship-it Oct 29, 2026
0b89103
feat(harness): execution links, --open and --verbose for try_agentic.py
chrishagglund-ship-it Oct 29, 2026
55944b6
start agent in a way that spawns stop_when worker
chrishagglund-ship-it Oct 29, 2026
ded4ecb
feat(harness): --stop-same-turn and a report built from the execution
chrishagglund-ship-it Oct 30, 2026
21f2362
feat(harness): rewire stop_when in harness runs by default
chrishagglund-ship-it Nov 2, 2026
e4ba43c
feat(harness): report each agentic run's outcome and story
chrishagglund-ship-it Nov 3, 2026
3037c34
feat(harness): single-replica agentic deployment for certification-aws
chrishagglund-ship-it Nov 3, 2026
37a6c50
feat(harness): end agentic runs the harness can't finish correctly
chrishagglund-ship-it Nov 3, 2026
2f6f131
ci(agent-e2e): cap pytest below 9.2 to keep xdist groups working
chrishagglund-ship-it Nov 9, 2026
9d9813c
helper script to walk through wf and show how story evolved until wf …
chrishagglund-ship-it Nov 10, 2026
b472fa5
ci(agent-e2e): fail fast with a named cause when the LLM path is broken
chrishagglund-ship-it Nov 10, 2026
888db13
ci(agent-e2e): check LLM providers first, drop the canary agent run
chrishagglund-ship-it Nov 11, 2026
8a664e2
ci(agent-e2e): run the suites on gpt-6-luna
chrishagglund-ship-it Nov 11, 2026
10968d8
TEMP: run test_image_openai to confirm its OpenAI error
chrishagglund-ship-it Nov 11, 2026
5bcc918
e2e: skip test_image_openai, dall-e-3 is gone from OpenAI's API
chrishagglund-ship-it Nov 11, 2026
71335b7
TEMP: run test_audio_openai to see its OpenAI error
chrishagglund-ship-it Nov 11, 2026
9d7fc73
e2e: skip test_audio_openai, the CI OpenAI project can't use tts-1
chrishagglund-ship-it Nov 11, 2026
af3940e
e2e: note the image test's gpt-image-* models aren't enabled in CI
chrishagglund-ship-it Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion .github/workflows/agent-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,44 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CONDUCTOR_SERVER_URL: http://localhost:8080/api
# Default model for every suite. The CI OpenAI project only allows the
# gpt-5.6 / gpt-6 families; gpt-6-luna is OpenAI's replacement for
# gpt-4o-mini, which shuts down on 2027-01-14.
CONDUCTOR_AGENT_LLM_MODEL: openai/gpt-6-luna
steps:
# A dead key, exhausted quota, retired model or provider outage makes
# every agent test fail at its client timeout, three times over with
# reruns — the job then hits timeout-minutes before pytest prints a
# single traceback. Call each model the suites use directly, before any
# setup: the HTTP status and body name the cause in seconds.
- name: LLM provider check
timeout-minutes: 1
run: |
fail=0
probe() { # label, url, curl args...
local label=$1 url=$2; shift 2
local code
code=$(curl -sS -m 15 -o "$RUNNER_TEMP/probe.json" -w '%{http_code}' "$url" "$@" \
-H 'content-type: application/json') || true
echo "$label: HTTP $code"
if [ "$code" != 200 ]; then
echo "::error::$label check failed (HTTP $code; 000 = no response in 15s)"
cat "$RUNNER_TEMP/probe.json" 2>/dev/null || true; echo
fail=1
fi
}
# Responses API, as the server uses; 16 is its minimum max_output_tokens.
openai_model=${CONDUCTOR_AGENT_LLM_MODEL#openai/}
probe "OpenAI $openai_model" https://api.openai.com/v1/responses \
-H "authorization: Bearer $OPENAI_API_KEY" \
-d "{\"model\":\"$openai_model\",\"input\":\"ping\",\"max_output_tokens\":16,\"reasoning\":{\"effort\":\"none\"}}"
for model in claude-sonnet-4-6 claude-sonnet-4-5; do
probe "Anthropic $model" https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' \
-d "{\"model\":\"$model\",\"max_tokens\":5,\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}]}"
done
exit $fail

- name: Checkout code
uses: actions/checkout@v4

Expand Down Expand Up @@ -69,7 +106,12 @@ jobs:
# mcp 2.0 dropped mcp.server.fastmcp, which mcp-testkit's server and
# tool modules still import — hold the resolver on the 1.x line until
# the testkit ships a 2.x-compatible release.
pip install pytest pytest-asyncio pytest-xdist pytest-rerunfailures mcp-testkit 'mcp<2'
# pytest 9.2.0 silently breaks --dist=loadgroup with pytest-xdist 3.8.0:
# xdist_group marks are ignored, so grouped suites (shared ports, shared
# server credentials) run in parallel and collide. Fixed on xdist master
# (pytest-dev/pytest-xdist#1388), not yet released. Once a pytest-xdist
# release after 3.8.0 is out, drop the pytest cap and require that xdist.
pip install 'pytest<9.2' pytest-asyncio pytest-xdist pytest-rerunfailures mcp-testkit 'mcp<2'

- name: Start mcp-testkit
run: |
Expand Down Expand Up @@ -115,6 +157,16 @@ jobs:
pytest e2e/ -v --tb=short -n 3 --dist=loadgroup \
--junitxml=results/junit-e2e.xml

# The uploaded server.log is lost when the run is re-run, and a run
# killed at timeout-minutes never prints pytest's tracebacks — keep the
# server's own errors in the job log, which survives both.
- name: Show server errors
if: failure() || cancelled()
run: |
[ -f server.log ] || exit 0
grep -nE ' (ERROR|WARN) |Exception|[Qq]uota|rate.?limit|\b(401|403|404|429)\b' server.log \
| grep -vE 'cannot init (Cohere|Mistral)' | tail -150

# 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.
Expand Down
25 changes: 15 additions & 10 deletions e2e/test_suite11_langgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
TIMEOUT = 120
SERVER_URL = os.environ.get("CONDUCTOR_SERVER_URL", "http://localhost:8080/api")
BASE_URL = SERVER_URL.rstrip("/").replace("/api", "")
# LangChain takes the bare OpenAI model name. No temperature: reasoning
# models (gpt-6-luna) reject it unless reasoning effort is "none".
OPENAI_MODEL = os.environ.get(
"CONDUCTOR_AGENT_LLM_MODEL", "openai/gpt-4o-mini"
).removeprefix("openai/")


# ═══════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -88,7 +93,7 @@ def multiply(a: int, b: int) -> str:
# Module-level LLM instance — needed for StateGraph node functions that
# reference `llm` via closure. The LLM detection in the serializer finds
# module-level variables via func.__globals__ but not closure variables.
_module_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
_module_llm = ChatOpenAI(model=OPENAI_MODEL)

# Tests
# ═══════════════════════════════════════════════════════════════════════════
Expand All @@ -104,7 +109,7 @@ def test_framework_detection(self):
"""create_react_agent graph -> detect_framework returns 'langgraph'."""
from langchain.agents import create_agent

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm = ChatOpenAI(model=OPENAI_MODEL)
graph = create_agent(llm, tools=[], name="detect_test")

framework = detect_framework(graph)
Expand All @@ -119,7 +124,7 @@ def test_hello_world_full_extraction(self):
"""create_agent(llm, tools=[]) serializes to full extraction path."""
from langchain.agents import create_agent

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm = ChatOpenAI(model=OPENAI_MODEL)
graph = create_agent(llm, tools=[], name="hello_world_test")

raw_config, workers = serialize_langgraph(graph)
Expand Down Expand Up @@ -164,7 +169,7 @@ def test_react_tools_full_extraction(self):
"""create_agent(llm, tools=[calculate, count_words]) -> full extraction."""
from langchain.agents import create_agent

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm = ChatOpenAI(model=OPENAI_MODEL)
graph = create_agent(
llm, tools=[calculate, count_words], name="react_tools_test"
)
Expand All @@ -175,7 +180,7 @@ def test_react_tools_full_extraction(self):
assert "model" in raw_config, (
f"[React] 'model' missing. Keys: {list(raw_config.keys())}"
)
assert "gpt-4o-mini" in str(raw_config["model"]), (
assert OPENAI_MODEL in str(raw_config["model"]), (
f"[React] Wrong model: {raw_config['model']}"
)

Expand Down Expand Up @@ -478,7 +483,7 @@ def test_tool_schema_is_json_schema(self):
"""React agent tool parameters must be valid JSON Schema."""
from langchain.agents import create_agent

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm = ChatOpenAI(model=OPENAI_MODEL)
graph = create_agent(
llm, tools=[calculate, multiply], name="schema_test"
)
Expand Down Expand Up @@ -533,7 +538,7 @@ def test_compile_hello_world_via_server(self):

from langchain.agents import create_agent

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm = ChatOpenAI(model=OPENAI_MODEL)
graph = create_agent(llm, tools=[], name="compile_hello_world")

raw_config, _workers = serialize_langgraph(graph)
Expand All @@ -556,7 +561,7 @@ def test_compile_react_tools_via_server(self):

from langchain.agents import create_agent

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm = ChatOpenAI(model=OPENAI_MODEL)
graph = create_agent(
llm, tools=[calculate, count_words], name="compile_react_tools"
)
Expand All @@ -579,7 +584,7 @@ def test_compile_stategraph_via_server(self):
if not _server_available():
pytest.skip("Server not available")

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm = ChatOpenAI(model=OPENAI_MODEL)

class State(TypedDict):
query: str
Expand Down Expand Up @@ -655,5 +660,5 @@ def _make_react_agent_with_multiply():
"""Build a react agent with multiply tool for runtime execution."""
from langchain.agents import create_agent

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm = ChatOpenAI(model=OPENAI_MODEL)
return create_agent(llm, tools=[multiply], name="e2e_lg_runtime")
5 changes: 3 additions & 2 deletions e2e/test_suite25_media_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
]

TIMEOUT = 120
OPENAI_MODEL = os.environ.get("CONDUCTOR_AGENT_LLM_MODEL", "openai/gpt-4o-mini")

# ── Test image (self-contained) ───────────────────────────────────────────────
# A 600x200 PNG rendering the exact text "MELON7391" (black on white), committed
Expand Down Expand Up @@ -82,7 +83,7 @@

# Positive test: Anthropic is skipped (no image reaches the model — see above).
POSITIVE_CASES = [
pytest.param("OPENAI_API_KEY", "openai/gpt-4o-mini", id="openai"),
pytest.param("OPENAI_API_KEY", OPENAI_MODEL, id="openai"),
pytest.param(
"ANTHROPIC_API_KEY",
"anthropic/claude-sonnet-4-5",
Expand All @@ -94,7 +95,7 @@
# Counterfactual: both providers should COMPLETE and simply not emit the token
# (no media is sent at all), so neither is expected to fail.
COUNTERFACTUAL_CASES = [
pytest.param("OPENAI_API_KEY", "openai/gpt-4o-mini", id="openai"),
pytest.param("OPENAI_API_KEY", OPENAI_MODEL, id="openai"),
pytest.param("ANTHROPIC_API_KEY", "anthropic/claude-sonnet-4-5", id="anthropic"),
]

Expand Down
6 changes: 5 additions & 1 deletion e2e/test_suite7_media_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ class TestSuite7MediaTools:

# ── Image: OpenAI ─────────────────────────────────────────────────

@pytest.mark.xfail(reason="OpenAI removed dall-e-2 default; model passthrough issue in Conductor runtime")
@pytest.mark.skip(
reason="dall-e-3 has been removed from OpenAI's API ('model does not exist'), and "
"its gpt-image-* replacements are not enabled in the CI OpenAI project"
)
def test_image_openai(self, runtime, model):
"""Generate image via OpenAI DALL-E 3."""
if not os.environ.get("OPENAI_API_KEY"):
Expand Down Expand Up @@ -175,6 +178,7 @@ def test_image_gemini(self, runtime, model):

# ── Audio: OpenAI ─────────────────────────────────────────────────

@pytest.mark.skip(reason="The CI OpenAI project does not have access to tts-1 (HTTP 403)")
def test_audio_openai(self, runtime, model):
"""Generate audio via OpenAI TTS-1."""
if not os.environ.get("OPENAI_API_KEY"):
Expand Down
Loading
Loading