feat(keycardai-crewai): new package split from keycardai-agents (ACC-231)#106
feat(keycardai-crewai): new package split from keycardai-agents (ACC-231)#106Larry-Osakwe wants to merge 5 commits into
Conversation
…-231) Per the KEP "Decompose keycardai-agents", the CrewAI-over-A2A integration moves out of keycardai-agents into a new keycardai-crewai package, sister to keycardai-a2a (#105 / ACC-230). Public surface in keycardai.crewai: - CrewAIExecutor — adapter that runs a CrewAI Crew factory and returns the result as a string. Intended to be called from inside an a2a-sdk AgentExecutor.execute method. - set_delegation_token(access_token) — stash the inbound bearer in a contextvar so synchronous CrewAI tool calls can pick it up at delegation time. - get_a2a_tools(service_config, delegatable_services) — return a list of crewai.tools.BaseTool instances, one per delegatable service. Each tool calls the target service via keycardai-a2a is DelegationClientSync.invoke_service. - create_a2a_tool_for_service(service_config, target_service_url) — single-tool variant for callers that already know the target URL. The body is the same code that lived at keycardai.agents.integrations.crewai with three string-level changes: - ImportError message updated to "pip install keycardai-crewai". - Docstring usage examples updated to "from keycardai.crewai import ...". - Module path in test mock targets ("patch(...ServiceDiscovery)") updated to "keycardai.crewai.ServiceDiscovery". Wrap rules (mirrored from keycardai-a2a): 1. Use CrewAI native Crew and BaseTool types directly. No parallel protocol. 2. Use keycardai-a2a native primitives (DelegationClientSync, ServiceDiscovery, AgentServiceConfig, KeycardServerCallContextBuilder) directly. 3. The bearer-token contextvar bridges from ServerCallContext.state["access_token"] (set by keycardai-a2a is KeycardServerCallContextBuilder) into the synchronous CrewAI tool execution path. 4. No new HTTP endpoints, no new wire formats. Customers compose CrewAIExecutor with keycardai-a2a primitives in their own Starlette or FastAPI app. The CrewAI-over-MCP-tools integration in keycardai-mcp is a separate concern (different protocol, different package) and stays put. Followups: - Empty out keycardai-agents (next commit on this branch). - ACC-232 archives the keycardai-agents source directory. - Refactor CrewAIExecutor to subclass a2a.server.agent_execution.AgentExecutor (the 1.x async base) so it can be passed straight to DefaultRequestHandler without an outer wrapper. - Update the keycardai-crewai row in the ACC-216 feature parity matrix.
…e (ACC-231) Per the KEP "Decompose keycardai-agents", the final piece of code in this package - the CrewAI-over-A2A integration at keycardai.agents.integrations.crewai - moved to keycardai-crewai in the preceding commit. This commit empties out the package. Deleted: - packages/agents/src/keycardai/agents/integrations/ (whole directory) - packages/agents/tests/integrations/ (whole directory) Reduced packages/agents/pyproject.toml: - Dropped [project.optional-dependencies].crewai (the crewai>=0.86.0 extra) - Dropped keycardai-a2a from core deps (no consumer left in this package) - Dropped pydantic from core deps (same) - Trimmed test/dev extras to the minimum needed for an empty package Reduced packages/agents/src/keycardai/agents/__init__.py to a deprecation-only docstring with no re-exports. Replaced packages/agents/README.md with an upgrade-path table pointing the three concerns at their new homes: - A2A delegation -> keycardai-a2a (#105 / ACC-230) - PKCE user-login -> keycardai-oauth (#101 / ACC-229) - CrewAI-over-A2A -> keycardai-crewai (this PR / ACC-231) Hard cut, no transitional bridge or re-export shim. The Explore inventory confirms zero runtime imports of keycardai.agents.integrations.crewai outside packages/agents/ itself, matching the precedent set by #105. ACC-232 will archive the packages/agents/ source directory in a follow-up PR. BREAKING: - from keycardai.agents.integrations.crewai import ... is gone; use from keycardai.crewai import ... . - The keycardai-agents[crewai] extra no longer exists; install keycardai-crewai directly.
This comment was marked as outdated.
This comment was marked as outdated.
…allel surface (ACC-231) Addresses fresh-eyes review on PR #106. The previous commit moved CrewAIExecutor verbatim, which carried the 0.x sync execute(task, inputs) signature and forced the README quickstart to hand-write the exact parallel-protocol bridge that #105 was reshaped to remove. Ships as the 0.1.0 PyPI surface, the README example becomes the canonical pattern users copy, so the gap had to close before publish. CrewAIExecutor now subclasses a2a.server.agent_execution.AgentExecutor with async execute(self, context, event_queue) and slots straight into DefaultRequestHandler(agent_executor=...). The README quickstart drops the hand-written async wrapper. execute() does four things in order: 1. Read context.call_context.state["access_token"] (set by keycardai.a2a.KeycardServerCallContextBuilder), call set_delegation_token so synchronous CrewAI tools see it; warn if absent so misconfigured deployments are visible in logs. 2. Build a fresh Crew via the factory. 3. Run crew.kickoff() on a worker thread via asyncio.to_thread. crew.kickoff is synchronous and CPU/IO-bound for seconds at a time; calling it directly inside an async def starves uvicorn. The contextvar set in step 1 propagates into the worker via contextvars.copy_context (which asyncio.to_thread does for us), so ServiceDelegationTool._run can still read the token. The class docstring spells this out so a future reader does not swap to_thread for a raw ThreadPoolExecutor. 4. Wrap the string result in an A2A Message (Role.ROLE_AGENT) and enqueue it. Drops the dead set_token_context flag and set_token_for_delegation method on CrewAIExecutor: both existed only for the Keycard AgentExecutor protocol that #105 deleted. Nothing in the new world calls them. Drops the dead try/except around `from crewai import Crew`: crewai is a hard dependency in pyproject.toml, not an extra, so the except branch is unreachable. Trims the Topic :: Security classifier from pyproject.toml: copy-paste from keycardai-a2a, but a CrewAI integration is not security software. Adds 8 new tests (TestCrewAIExecutor + TestSetDelegationToken): - subclass check (isinstance(executor, AgentExecutor)) - execute runs crew.kickoff with the user input as task - execute enqueues a Message with the crew result - execute propagates access_token from RequestContext into the contextvar, observed from inside crew.kickoff via asyncio.to_thread context inheritance - execute warns when access_token is absent - execute does not block the event loop (probe inside kickoff confirms asyncio.get_running_loop() raises in the worker thread) - cancel returns None - set_delegation_token writes to the public contextvar Tests: crewai 24 passed (was 16; +8) a2a 44 passed agents 0 collected (empty package, expected) ruff clean
This comment was marked as outdated.
This comment was marked as outdated.
…gh DefaultRequestHandler over JSONRPC (ACC-231)
The unit tests in test_crewai_a2a.py prove CrewAIExecutor.execute does
the right thing when called directly with mocked context and event_queue.
They do not prove that DefaultRequestHandler actually invokes execute
when a real JSONRPC SendMessage request lands on the mount. That gap is
the highest-value thing to close before this hits PyPI; the README quickstart
shape (DefaultRequestHandler(agent_executor=CrewAIExecutor(...), ...)) is
what users will copy, so a regression here is invisible until somebody
actually deploys the package.
This module instantiates the headline composition the way the README
shows it, drives it with a real JSONRPC POST through Starlette is
TestClient, and asserts the crew result comes back.
Three assertions:
1. The user message text reaches crew.kickoff(inputs={"task": ...}).
If the dispatcher does not invoke our execute method, this fails.
2. The crew result string appears in the JSONRPC response body. Verifies
the Message we enqueue is shaped correctly for SendMessageResponse.
3. The KeycardUser access_token reaches the contextvar by the time
crew.kickoff runs. End-to-end check of the full chain:
KeycardUser -> request.scope["user"] -> KeycardServerCallContextBuilder
-> ServerCallContext.state["access_token"] -> CrewAIExecutor.execute
-> set_delegation_token -> asyncio.to_thread context inheritance
-> crew.kickoff observes it.
Auth uses a stub backend that always returns a fixed KeycardUser. Real
auth is exercised in keycardai-starlette is own tests; a real verifier
here would need a reachable JWKS endpoint, and the verifier-to-context
flow is already covered by the propagation tests in keycardai-a2a. The
isolation lets this test focus on the wire-up between the JSONRPC
dispatcher and our executor.
Two non-obvious bits learned while wiring this up, both worth knowing:
- a2a-sdk 1.x JSONRPC method names are CamelCase ("SendMessage"), not the
0.x slash form ("message/send"). The keycardai-a2a tests at
packages/a2a/tests/test_agent_card_server.py use "message/send" but
only assert 401 from the auth gate, so the dispatcher never sees the
body and the wrong method name was never caught there.
- The dispatcher requires an A2A-Version: 1.0 header on the request. Without
it the server defaults to 0.3 and validate_version returns a JSONRPC error
before the executor is ever called.
Both worth following up on the keycardai-a2a side (the 401 tests should
also cover at least one positive path so this kind of dispatcher-shape
drift gets caught), but out of scope for this PR.
Tests:
crewai 27 passed (24 unit + 3 integration)
a2a 44 passed
agents 0 collected (empty package, expected)
oauth/starlette/mcp/mcp-fastmcp/fastmcp green
ruff clean
This comment was marked as outdated.
This comment was marked as outdated.
…dispatcher (ACC-231) DelegationClient.invoke_service was hardcoded to the 0.x JSONRPC method "message/send", emitted a 0.x message envelope (lowercase role, no messageId), omitted the A2A-Version header, and unwrapped a 0.x response shape (result.parts). Against any real 1.x server the dispatcher rejected the call before the executor ran, so the keycardai-crewai delegation tools (the entire client-side path that runs through DelegationClientSync.invoke_service) were dead-on-arrival. The bug survived because the existing test_a2a_client.py tests mock the http client and the request body never sees a real dispatcher; they asserted "method == message/send" against the stubbed wire shape, which was internally consistent and externally wrong. Caught by the keycardai-crewai integration test added in commit 4: that test sends a real JSONRPC POST through Starlette TestClient against the real a2a-sdk DefaultRequestHandler, so dispatcher contract drift fails fast. Changes: - _build_jsonrpc_message_send -> _build_jsonrpc_send_message: method name is now "SendMessage" (the 1.x CamelCase form), the message envelope carries a messageId (required by the dispatcher) and the canonical enum-string role "ROLE_USER". - Both async and sync invoke_service set the A2A-Version: 1.0 header. The header constant comes from a2a.utils.constants so a future a2a-sdk rename follows our code automatically. - _unwrap_jsonrpc_response unwraps result.message.parts[].text, the shape SendMessageResponse takes when the executor enqueues a Message. Tasks fall back to JSON-stringified for now; full Task lifecycle consumers should reach for a2a.client.create_client directly. Tests: test_a2a_client.py mocks updated to the 1.x response shape and now assert method == "SendMessage", role == "ROLE_USER", messageId presence, and A2A-Version header on the outbound POST. Added test_jsonrpc_dispatch.py: a positive-path TestClient test that drives a real a2a-sdk DefaultRequestHandler with a stub AgentExecutor enqueuing a Message, then asserts the executor saw the user input and the access_token from KeycardServerCallContextBuilder. The auth-gate tests in test_agent_card_server.py only cover 401, so dispatcher contract drift had no local guard. This test fills the gap on the keycardai-a2a side; the keycardai-crewai integration test added in commit 4 covers the same chain with a CrewAIExecutor on top. a2a 45 passed (was 44; +1) crewai 27 passed agents 0 collected (empty package, expected) oauth/starlette/mcp/mcp-fastmcp/fastmcp green ruff clean BREAKING: Wire shape changes; any caller depending on the old "message/send" method name or the lowercase "user" role on the outbound envelope needs to update. No production callers exist (closed alpha; the keycardai-crewai delegation path is the only consumer in this PR).
📦 Release PreviewThis analysis shows the expected release impact: 📈 Expected Version Changes📋 Package Details[
{
"package_name": "keycardai-agents",
"package_dir": "packages/agents",
"has_changes": true,
"current_version": "0.2.0",
"next_version": "0.3.0",
"increment": "MINOR"
},
{
"package_name": "keycardai-a2a",
"package_dir": "packages/a2a",
"has_changes": true,
"current_version": "0.2.0",
"next_version": "0.3.0",
"increment": "MINOR"
},
{
"package_name": "keycardai-crewai",
"package_dir": "packages/crewai",
"has_changes": true,
"current_version": "0.0.0",
"next_version": "0.1.0",
"increment": "MINOR"
}
]📝 Changelog PreviewThis comment was automatically generated by the release preview workflow. |
|
Closing without merging. After review (see Slack thread), we decided not to ship Doc coverage for the CrewAI integration (and the same template for LangChain, etc.) folds into the upcoming docs overhaul. One independent bug surfaced while doing this work: |
Removes packages/agents/ entirely. The PKCE user-login client moved to keycardai-oauth (#101); the A2A delegation surface moved to keycardai-a2a (#105). The CrewAI integration that briefly remained had no users; the extraction PR (#106) was closed without merging in favor of customers wiring CrewAI to Keycard primitives directly. Drops keycardai-agents from the root workspace sources and the release.yml tag allowlist so accidental tags cannot trigger republish. keycardai-agents on PyPI is frozen at 0.2.0 and is being yanked separately. Updates packages/a2a/README.md to drop the now-stale references to a forthcoming keycardai-crewai package and the in-progress archival. BREAKING: any "from keycardai.agents.*" import fails. No production users. Closes ACC-232.
Summary
Per the KEP Decompose keycardai-agents, extracts the CrewAI-over-A2A integration into a new
keycardai-crewaipackage, sister tokeycardai-a2a(#105). After this PR,keycardai-agentsships no source; ACC-232 archives the directory.The CrewAI-over-MCP-tools integration in
keycardai-mcpis a separate concern and is untouched.Public surface in
keycardai.crewai:CrewAIExecutor—a2a-sdk1.xAgentExecutorsubclass. Pass directly toDefaultRequestHandler(agent_executor=...). Reads the bearer fromcontext.call_context.state["access_token"], runscrew.kickoff()on a worker thread viaasyncio.to_thread, enqueues the result as aMessage.set_delegation_token(access_token)— public hook for code paths running a crew outsideCrewAIExecutor.get_a2a_tools(service_config, delegatable_services)— list ofcrewai.tools.BaseToolper delegatable service. Each calls the target viakeycardai-a2a'sDelegationClientSync.invoke_service.create_a2a_tool_for_service(service_config, target_service_url)— single-tool variant.Five commits on this branch
feat(keycardai-crewai)!:— new package skeleton atpackages/crewai/. Body ofkeycardai.agents.integrations.crewaimoves intokeycardai.crewai.__init__.py; tests andrelease.ymlallowlist updated; workspace registered in repo-rootpyproject.toml.refactor(keycardai-agents)!:— empty out the package: deletesintegrations/, reduces deps to[], replaces README with an upgrade-path table.refactor(keycardai-crewai)!:— addresses fresh-eyes review.CrewAIExecutorreconciled to subclassAgentExecutorwith asyncexecute. README quickstart drops the hand-written bridge wrapper.crew.kickoffruns viaasyncio.to_thread. Drops dead code (set_token_contextflag,set_token_for_delegation,try/exceptaround thecrewaiimport,Topic :: Securityclassifier). +8 unit tests.test(keycardai-crewai):— integration test through the real JSONRPC dispatcher. Sends aSendMessagePOST throughTestClientagainstDefaultRequestHandler(agent_executor=CrewAIExecutor(...))and asserts the crew is driven, the result reaches the response, and the bearer reaches the contextvar.fix(keycardai-a2a)!:—DelegationClient.invoke_servicewas using the 0.x JSONRPC method (message/send), 0.x message envelope (lowercase role, nomessageId), and missing theA2A-Version: 1.0header. Against any real 1.x server the dispatcher rejected the call. Caught by commit 4. Fixes the wire shape, updates_unwrap_jsonrpc_responsefor the 1.xSendMessageResponse.message.partsshape, and adds a positive-path JSONRPC test on the keycardai-a2a side so future drift is local.Hard cut, no transitional bridge
The Explore inventory confirms zero runtime imports of
keycardai.agents.integrations.crewaioutsidepackages/agents/itself. No re-export shim ships.Reviewer notes
packages/agents/tests/integrations/test_crewai_a2a.pymove topackages/crewai/tests/test_crewai_a2a.pywith imports rewritten. Same coverage, same count.keycardai-agentskeeps its[tool.commitizen]block so it can still be tagged for the bump (next bump 0.2.0 → 0.3.0 — breaking, since deps and surface were reduced).packages/a2a/examples/keycard_protected_server/main.py:54-57referencesMessageRole.MESSAGE_ROLE_AGENT, which doesn't exist in installeda2a-sdk1.x (the enum isRole.ROLE_AGENT). Out of scope here; worth a follow-up.Test plan
crewai: 27 passed (24 unit + 3 integration)a2a: 45 passed (was 44, +1 positive-path JSONRPC dispatch)agents: 0 collected (empty)oauth,starlette,mcp,mcp-fastmcp,fastmcp: greenuv run ruff checkcleanfrom keycardai.crewai import set_delegation_token, get_a2a_tools, create_a2a_tool_for_service, CrewAIExecutorsucceeds;isinstance(CrewAIExecutor(lambda: None), AgentExecutor)is True;from keycardai.agents.integrations.crewai import set_delegation_tokenraisesModuleNotFoundErrorPre-merge checklist
keycardai-crewaiin the keycard org. Form fields: projectkeycardai-crewai, ownerkeycardai, repopython-sdk, workflowrelease.yml, environmentpypi-release.packages/a2a/CHANGELOG.mdfrom feat(keycardai-a2a): new package split from keycardai-agents (ACC-230) #105).Follow-ups
packages/agents/source directory.MessageRolereference inpackages/a2a/examples/keycard_protected_server/main.py.crewai>=0.86.0,<2.0defensively (silent-import-break insurance).@auth.grant(resource)decorator parity withkeycardai-mcp.keycardai-crewairow in the ACC-216 feature parity matrix.