Repository navigation
Conversation
Adds conformance/agent/agent_functions.yaml for agent functions and the agent's side of function RPC: declaring agent functions on a catalog, answering callAgentFunction, validating model output that calls agent functions, and calling renderer functions on a fake clock. The Python agent SDK runs it; the TypeScript and Dart agent SDKs and the web_core runner skip its actions with a reason. Also adds a message_processor_v1_0.yaml case: under strict validation, a call to a function the surface catalog declares schema-only is accepted. Signed-off-by: Nan Yu <nanyu@google.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the conformance test suite for agent functions and renderer function calls (agent_functions.yaml), along with its documentation, schema definitions, and a Python test runner implementation. Placeholder/skip tests are added for Dart and TypeScript since they do not yet support agent functions. Feedback on the Python test runner recommends initializing self._now_ms before calling super().init() in _FakeClockLoop to prevent an AttributeError, checking if a future is done before calling future.exception() to avoid asyncio.InvalidStateError, and explicitly setting the custom event loop using asyncio.set_event_loop to prevent potential runtime errors.
| def __init__(self) -> None: | ||
| super().__init__(selectors.DefaultSelector()) | ||
| self._now_ms = 0 |
There was a problem hiding this comment.
When subclassing asyncio.SelectorEventLoop, the parent constructor (super().__init__) can invoke overridden methods like time(). If self._now_ms is initialized after super().__init__(), it can lead to an AttributeError during initialization. Please initialize self._now_ms before calling super().__init__().
| def __init__(self) -> None: | |
| super().__init__(selectors.DefaultSelector()) | |
| self._now_ms = 0 | |
| def __init__(self) -> None: | |
| self._now_ms = 0 | |
| super().__init__(selectors.DefaultSelector()) |
There was a problem hiding this comment.
Done in 38bb93d: _now_ms is set before super().__init__().
🤖 Agent-generated
| for future in futures.values(): | ||
| if not future.cancelled(): | ||
| future.exception() |
There was a problem hiding this comment.
Calling future.exception() on a future that is not yet done will raise an asyncio.InvalidStateError. To prevent potential runtime errors, ensure the future is done before checking its exception.
| for future in futures.values(): | |
| if not future.cancelled(): | |
| future.exception() | |
| for future in futures.values(): | |
| if future.done() and not future.cancelled(): | |
| future.exception() |
There was a problem hiding this comment.
Done in 38bb93d. dispose() settles every pending call before this loop, so it couldn't raise today, but the future.done() guard costs nothing and keeps it safe if that changes. I also added a comment on why the loop reads the exceptions at all (so asyncio doesn't log "exception was never retrieved").
🤖 Agent-generated
| def _run_calls(case: Mapping[str, Any]) -> None: | ||
| loop = _FakeClockLoop() | ||
| try: | ||
| loop.run_until_complete(_run_calls_on_loop(case, loop)) | ||
| finally: | ||
| loop.close() |
There was a problem hiding this comment.
When running a custom event loop manually via loop.run_until_complete, it is a best practice to set it as the current thread's event loop using asyncio.set_event_loop(loop). This prevents potential RuntimeErrors or "attached to a different loop" errors if any downstream code or library calls asyncio.get_event_loop().
| def _run_calls(case: Mapping[str, Any]) -> None: | |
| loop = _FakeClockLoop() | |
| try: | |
| loop.run_until_complete(_run_calls_on_loop(case, loop)) | |
| finally: | |
| loop.close() | |
| def _run_calls(case: Mapping[str, Any]) -> None: | |
| loop = _FakeClockLoop() | |
| asyncio.set_event_loop(loop) | |
| try: | |
| loop.run_until_complete(_run_calls_on_loop(case, loop)) | |
| finally: | |
| loop.close() | |
| asyncio.set_event_loop(None) |
There was a problem hiding this comment.
Done in 38bb93d: the loop is installed with asyncio.set_event_loop while the case runs and cleared afterwards.
🤖 Agent-generated
Set the fake clock before the base event loop constructor runs, install the loop as the current event loop while a case runs, and only read failures of futures that are done. Signed-off-by: Nan Yu <nanyu@google.com>
…into test/conformance-agent-function-rpc
…into test/conformance-agent-function-rpc
…into test/conformance-agent-function-rpc Signed-off-by: Nan Yu <nanyu@google.com>
Stacked on #3793. Review only the top commit (
test(conformance): agent function RPC).Part of #3536. Part of #3535.
Description
#3792 and #3793 added agent functions and the agent's RPC helpers to the Python SDKs, tested only by Python unit tests. This PR writes their behaviour down as a shared conformance suite, so the TypeScript and Dart agent SDKs can implement against the same cases later.
agent/agent_functions.yaml(38 cases). A case declares each agent function with the JSON Schema of itsargsand a scriptedbehaviorin place of a handler:returns: <value>,returns_args: true,returns_context: true, orraises: {message, code?}. That way every SDK runs the same function. The suite has four actions:declare_agent_functions: an agent function is listed among the catalog's functions and in itstoJson()document. Taking the name of a function the catalog already declares is aCatalogError.handle_agent_function: answers onecallAgentFunctionmessage. The cases cover the value (includingnull) withfunctionCallIdcopied verbatim; schema defaults applied before the handler runs; the handler's context;INVALID_FUNCTION_CALLfor bad, missing or undeclared args and foragentOnlyfunctions;UNKNOWN_FUNCTIONfor an unknown name or catalog, or a renderer function of the catalog;EXECUTION_ERROR, or the handler's own code, when the handler fails; picking the catalog the call names; the rules for a call withoutcatalogId; and malformed messages with and without afunctionCallId.validate_payload: model output that calls a declared agent function validates. A call to an undeclared function, or with args the schema rejects, doesn't.call_renderer_function: steps (call,respond,advance_ms,dispose) run on a fake clock that onlyadvance_msmoves, so no case waits on real time. The cases cover the message a call builds; a response settling the matching call with a value or the renderer's error code; unknown, duplicate and late responses being ignored; the 30 s default timeout, per-call and per-instance timeouts, and no timeout after a call settles;disposefailing pending calls withCANCELLEDand refusing later calls withDISPOSED; andmax_pending(TOO_MANY_PENDING).conformance_schema.jsongets closed definitions for the four case shapes.README.mdlists the suite and adds "Writing cases for the agent function suite". The harness guide gets a stage row, a short section on the fake clock, and the Python runner in its reference list.test_agent_function_conformance.pyruns every case. Behaviours become async handlers, and renderer calls run on anasyncioloop whosetime()onlyadvance_msmoves.UNIMPLEMENTED_ACTIONSinloader.ts, andagent-functions.test.tsregisters each case as skipped with that reason.agent_functions_conformance_test.dartdoes the same.agent/, so the four actions join itsUNIMPLEMENTED_ACTIONSas agent-side only.message_processor_v1_0.yaml: understrictMode: true, a call withoutcatalogIdto a function that the surface default catalog declares schema-only is accepted. The existing cases covered an undeclared function (accepted when not strict, rejected when strict) and a schema-only function reached through an explicitcatalogId, but not this one.Tests
uv run pytest python/a2ui_agent/tests python/a2ui_core/tests: 3451 passed, 7 skipped (the new suite: 39 passed).uv run mypy .,pyinkandscripts/fix_licenses.py --checkare clean.uv run pytest conformance/testsvalidates every suite against the schema.yarn testintypescript/agents/a2ui_agent(the suite: 1 passed, 38 skipped) andyarn test:conformanceintypescript/core/web_core(616/616, the new core case included).dart testindart/a2ui_agent(the suite: 1 passed, 38 skipped) anddart test test/conformanceindart/a2ui_core, which runs the new core case.MessageProcessorConformanceTests(run on Linux in theswift:6.1image). That run also failstest_v10_call_to_undeclared_function_without_strict_mode_accepted, which fails the same way on feat(python-agent): answer agent functions and call renderer functions #3793 without this PR.