Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request implements agent-side function routing (fallback routing of callAgentFunction) for v1.0 surfaces in the Swift core SDK. It introduces SchemaOnlyFunction to declare functions implemented on the agent, and updates DataContext, NodeResolver, SurfaceViewModel, and MessageProcessor to route calls without local implementations to the agent while managing in-flight requests, argument validation, and timeouts. Comprehensive unit tests have been added to verify this routing behavior. I have no additional feedback to provide as no review comments were submitted.
| _ = self.evaluateDynamicValue( | ||
| funcCallVal, | ||
| basePath: basePath, | ||
| index: index, | ||
| isUserActivated: true | ||
| ) |
There was a problem hiding this comment.
Action functionCall handlers run outside a tree-resolution pass (currentNodeAgentCalls == nil, so tracksNode == false). If an action's functionCall passes a nested agent-routed call as an argument (for example openUrl(url: createCheckoutUrl()) or submitOrder(token: mintToken())), resolveArguments dispatches the inner agent call, sees pendingAgentCallCount increase, and returns nil, so the outer call never executes. When the inner call later settles, settleAgentCall returns early on guard tracksNode ... and discards the value without ever re-evaluating funcCallVal (unlike web_core and Python, which await nested arguments before invoking the outer action function).
Consider driving action functionCall evaluation through a transient per-action settlement loop (or async evaluation helper) so nested agent arguments settle and feed into the outer call.
Details and suggested fix
One lightweight way to keep DataContext synchronous while supporting nested agent arguments in actions is to evaluate funcCallVal against a transient per-invocation FunctionHandler wrapper (or per-action cache on NodeResolver) that records in-flight AgentFunctionCalls for that action click, awaits their settlement via agentFunctionCaller, caches their results for that invocation, and re-runs evaluateDynamicValue until no new pending calls are added (stopping if any inner call fails).
There was a problem hiding this comment.
Done in 56b2574. NodeResolver.runActionFunctionCall evaluates the action with user activation and records the agent calls it makes. When the evaluation was deferred because an argument was still pending, it waits for those calls to settle on the surface cache (the same request, never resent) and runs the action again. It stops if any argument failed, and that failure is reported once. Actions with only local functions still run synchronously in one pass. Tests: sendsActionFunctionCallOnceNestedAgentArgumentSettles, stopsActionFunctionCallWhenNestedAgentArgumentFails (a new click retries), and stopsActionFunctionCallWhenNestedArgumentFailsSynchronously.
🤖 Agent-generated
- Event actions resolve context entries one at a time, then userMessage,
and stop at the first failure without sending later calls. Actions report
only their first failure; late sibling failures are silent.
- A check waiting on an agent call is pending (ResolvedCheck.isPending,
Node.isValidationPending) and fails once the call fails.
- Replacing or clearing agentFunctionCaller drops in-flight calls and kept
answers; answers from the previous caller are ignored.
- CANCELLED calls report nothing. deleteSurface cancels with
FunctionError.surfaceCallsCancelled ("Pending calls for surface '<id>'
were cancelled."), and a deleted surface sends nothing.
- Calls whose arguments hold NaN or infinite numbers fail locally with
EXPRESSION_ERROR instead of being sent.
Refs #3741
Signed-off-by: Nan Yu <nanyu@google.com>
|
Aligned the Swift core with the cross-SDK fix list from the TS/Dart/Python reviews in 6559fa1. Each change has a test in
U1, U3, U6, U9, U10, U12–U16 were already correct, and I added tests where none existed. One Swift-specific limit: Foundation's Full Swift core suite under Linux (Docker swift:6.1): 602 tests, with the same 5 known harness issues as the baseline (583 tests). 🤖 Agent-generated |
|
Follow-up for U15 in f828ee6: when an event action's Tests: 🤖 Agent-generated |
…to a non-string
Matches web_core, Python and Dart: a userMessage expression that resolves
to a non-string, at once or after an agent call, stops the action with
INVALID_ACTION ("Action event 'userMessage' must resolve to a string.")
and nothing is dispatched.
Refs #3741
Signed-off-by: Nan Yu <nanyu@google.com>
|
Follow-up for U15 in 187e04d: a 🤖 Agent-generated |
A userMessage that resolves to null, at once or after an agent call, is dispatched without a user message and reports nothing. Only a non-null non-string reports INVALID_ACTION. Refs #3741 Signed-off-by: Nan Yu <nanyu@google.com>
|
Round 2 alignment with the cross-SDK list is in 6e0b134. Each behavior change has a test in Fixed:
Already correct, now with tests: V4 (processor The 🤖 Agent-generated |
- A CANCELLED call isn't remembered as failed; the next evaluation sends
it again (V1).
- Evaluations that can't wait never route: a one-shot DataContext read on
the surface only reads kept answers, and a function that resolves parts
itself (formatString ${...}) is pending until its agent calls settle (V3).
- Answers of calls that resolved nodes make are never evicted from the
256-entry cache, so they aren't sent again (V8).
- BREAKING: a local function that throws while an action runs is reported
as EXECUTION_ERROR instead of EXPRESSION_ERROR (V9).
- Schema-only arguments get nested object defaults (V11).
- Tests for processor dispose(), deleteSurface cancellation, concurrent
sibling arguments, event entry errors, RPC argument refusal and
__proto__ keys (V4, V5, V7, V12, V13, V14).
Refs #3741
Signed-off-by: Nan Yu <nanyu@google.com>
|
Follow-up in 90cf170, applying the round 2 reconciliation:
Full Swift core suite in Docker: 654 tests, with the same 5 known harness issues as the baseline. 🤖 Agent-generated |
- formatString ${...} parts (any call a running local function resolves)
that call an agent function are refused with EXPRESSION_ERROR; nothing
is sent and the part is empty. This replaces the waiting behavior.
- A malformed event context entry or userMessage stops the action with
EXPRESSION_ERROR "Action event context entry '<key>' is invalid: ..."
or "Action event userMessage is invalid: ...".
- An agent-initiated call whose nested argument fails without a message
responds "An argument of the function call could not be resolved."
- Tests that refusals in actions stay EXPRESSION_ERROR.
Refs #3741
Signed-off-by: Nan Yu <nanyu@google.com>
… agent answer Refs #3741 Signed-off-by: Nan Yu <nanyu@google.com>
…eys in event values - From v1.0, a local function call whose arguments don't match the function's schema is refused with EXPRESSION_ERROR and doesn't run, in bindings and actions; an event action with such an entry isn't dispatched. - An event context entry or userMessage holding an object with an unknown @-prefixed key (other than @path, @call, or an @@ escape) is malformed: EXPRESSION_ERROR "Action event context entry '<key>' is invalid: ...", nothing dispatched. Refs #3741 Signed-off-by: Nan Yu <nanyu@google.com>
|
Follow-up in d9e9fec for two gaps found by the shared conformance cases:
Tests are in 🤖 Agent-generated |
Signed-off-by: Nan Yu <nanyu@google.com>
a4ec5be to
622f88a
Compare
Stacked on the Python PR (#3740). Review only the top commit (
fix(swift-core): ...).Fixes #3689. Part of #3536.
Description
The Swift core did not route v1.0 function calls it cannot run to the agent. A binding to an unimplemented function reported "Function not found" even with an
outboundListener; only app code callingMessageProcessor.callAgentFunctionreached the agent. This PR ports the routing from #3735.Changes
AgentFunctionCall(name, resolvedcatalogID, args) andAgentFunctionCallerare new.SurfaceViewModel.agentFunctionCalleris the hook, andSurfaceViewModel.callAgentFunction(_:)shares one request between identical calls still awaiting a response.MessageProcessorinstalls the caller on v1.0 surfaces whileoutboundListeneris set. SinceoutboundListeneris mutable, setting or clearing it updates existing surfaces.agentFunctionTimeoutSecondsdefaults to 30.SchemaOnlyFunctionis new, andFunctionImplementation.hasLocalImplementation(defaulttrue) tells them apart.DataContextroutes a call whose catalog resolved but has no local implementation, with the resolved catalog id. For a declared function it enforcesrequiresUserActivation, applies top-level schema defaults and validates the arguments before sending. A call with a pending nested argument is not sent until that argument settles. An unresolvable catalog stays a catalog error; pre-1.0 surfaces and@system functions are not routed.NodeResolverkeeps evaluation synchronous: a routed call is.nullwhile pending. Agent calls are tracked per node, and when a response arrives only the nodes that made the call are re-resolved. Results are cached per call, so re-resolving a node for an unrelated change does not re-send a request. Errors and timeouts go through the existing function-error path asEXPRESSION_ERROR. Action function calls run with user activation and are always sent.callRendererFunctionfor a schema-only function still returnsINVALID_FUNCTION_CALL.ValidationConfigdoc comment and a new "Agent-side functions" section inswift/core/README.md.Testing
Swift is not available on the Linux host used for this change, so it was built and tested in Docker
swift:6.1with a scratch copy of the package that maps Combine to OpenCombine and drops the SwiftUI targets. The repository is unchanged by that setup.AgentFunctionRoutingTests(19 tests) mirror the web_core suite, plus check rules, user activation, and installing or removing the caller whenoutboundListenerchanges.Not verified locally: the Apple Combine build, macOS and iOS, and the SwiftUI targets. The new parameters and protocol requirements all have defaults, so those targets should compile unchanged; CI's
ios-buildcovers them.