Skip to content

Upgrade to AppFunctions 1.0.0-alpha10#24

Open
keyboardsurfer wants to merge 4 commits into
mainfrom
bw/alpha10
Open

Upgrade to AppFunctions 1.0.0-alpha10#24
keyboardsurfer wants to merge 4 commits into
mainfrom
bw/alpha10

Conversation

@keyboardsurfer

Copy link
Copy Markdown
Member

• Upgraded both agent and ChatApp modules to AppFunctions 1.0.0-alpha10.
• Refactored exception handling in AgentOrchestrator to act on errors directly instead of surfacing them as unhandled exceptions.
• Cleaned up obsolete repository classes and refactored base service architectures.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request migrates the application to the new androidx.appfunctions version 1.0.0-alpha10 architecture, replacing the legacy configuration with the new AppFunctionService entry points. It introduces BaseChatAppFunctionService, BaseBuiltInAppFunctionService, and BaseFakeAppFunctionService, registers them in the manifests, and adds AppFunctionExceptionFormatter to format exceptions for LLM outputs. Feedback focuses on ensuring cooperative coroutine cancellation by rethrowing CancellationException instead of swallowing or wrapping it in BaseChatAppFunctionService and AgentOrchestrator. Additionally, it is recommended to avoid using reflection with Class.forName when launching MainActivity in BaseChatAppFunctionService.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +130 to +139
val sentMessageId =
try {
messageRepository.send(
text = messageBody,
recipientIds = listOf(endpointValue),
imageUris = imageUris?.map { it.toString() },
)
} catch (e: Exception) {
throw AppFunctionAppUnknownException("Failed to send message: ${e.message}")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Swallowing or wrapping CancellationException in a generic AppFunctionAppUnknownException can break cooperative coroutine cancellation. In Kotlin coroutines, CancellationException should always be rethrown to allow the coroutine machinery to cancel the job properly.

Suggested change
val sentMessageId =
try {
messageRepository.send(
text = messageBody,
recipientIds = listOf(endpointValue),
imageUris = imageUris?.map { it.toString() },
)
} catch (e: Exception) {
throw AppFunctionAppUnknownException("Failed to send message: ${e.message}")
}
val sentMessageId =
try {
messageRepository.send(
text = messageBody,
recipientIds = listOf(endpointValue),
imageUris = imageUris?.map { it.toString() },
)
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
throw AppFunctionAppUnknownException("Failed to send message: ${e.message}")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +164 to +170
return PendingIntent.getActivity(
this,
0,
Intent(this, Class.forName("com.example.chatapp.MainActivity"))
.apply { putExtra("nav_route", "call/${recipient.id}") },
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Class.forName to resolve MainActivity is fragile and uses reflection, which can be slow and flagged by static analysis or R8. Since MainActivity is registered in the manifest, you can use Intent.setClassName directly with the class name string. This is more idiomatic and avoids reflection.

        val intent = Intent().setClassName(this, "com.example.chatapp.MainActivity")
            .apply { putExtra("nav_route", "call/${recipient.id}") }
        return PendingIntent.getActivity(
            this,
            0,
            intent,
            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
        )

Comment on lines +373 to +394
is ExecuteAppFunctionResult.Error -> {
val exception = executionResult.exception
val appException = AppFunctionExceptionFormatter.getAppFunctionException(exception)
if (appException != null) {
results.add(
ToolOutput(
functionId = toolCall.functionId,
callId = toolCall.callId,
result =
AppFunctionExceptionFormatter.format(
appException,
toolCall.functionId,
),
),
)
} else {
throw IllegalStateException(
"Tool execution failed for ${toolCall.functionId}: ${exception.message}",
exception,
)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the tool execution is cancelled, executionResult.exception will be a CancellationException. Wrapping CancellationException in an IllegalStateException breaks cooperative coroutine cancellation. You should check if the exception is a CancellationException and rethrow it directly.

                    is ExecuteAppFunctionResult.Error -> {
                        val exception = executionResult.exception
                        if (exception is kotlinx.coroutines.CancellationException) {
                            throw exception
                        }
                        val appException = AppFunctionExceptionFormatter.getAppFunctionException(exception)
                        if (appException != null) {
                            results.add(
                                ToolOutput(
                                    functionId = toolCall.functionId,
                                    callId = toolCall.callId,
                                    result =
                                        AppFunctionExceptionFormatter.format(
                                            appException,
                                            toolCall.functionId,
                                        ),
                                ),
                            )
                        } else {
                            throw IllegalStateException(
                                "Tool execution failed for ${toolCall.functionId}: ${exception.message}",
                                exception,
                            )
                        }
                    }

@@ -28,7 +28,7 @@ android {
defaultConfig {
applicationId = "com.example.chatapp.wear"
minSdk = 30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit/optional: Can we just bump this to 36? then we don't need all the RequiresApi (?)

@@ -47,7 +47,7 @@ class AppFunctionsViewModelTest {
fun toggleFunction_doesNotCrash() =
runTest {
// Use a potentially real ID from the app

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: seems this is fixed?

android:name="com.example.appfunctions.agent.data.FakeAppFunctionService"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
android:exported="true"
tools:targetApi="36">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple services won't work on Android 36 and is a 37+ feature

}
}
}
object AppFunctionsIds {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are available from ChatAppFunctionService too

recipientsRepo: RecipientsRepository,
callMgr: CallManager,
) : BaseChatAppFunctionService() {
init {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll suggest use AppFunctionManager API to test so that the service doesn't need to be faked. That will be our testing suggestion for app developer as well.

In general, the testing is done as following

  1. Use UiAutomation to adopt shell permission to obtain EXECUTE_APP_FUNCTION permission
  2. Use Jetpack AppFunctionManager to search AppFunctions from your target app package name. Then developers can assert the functions are exposed correctly.
  3. Use Jetpack AppFunctionManager to execute it.

This will ensure that the testing is e2e and represent how agent will use it. The mocking test has a problem that if developer setup the manifest or something incorrectly, it would not catch it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

)
is ExecuteAppFunctionResult.Error -> {
val exception = executionResult.exception
val appException = AppFunctionExceptionFormatter.getAppFunctionException(exception)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: appFunctionException

@Test
fun `invoke returns Error when AppFunctionManager is null`() = runTest {
val useCaseWithNullManager = ExecuteAppFunctionUseCase(null, convertAppFunctionDataToJsonUseCase)
val function = mockk<AppFunctionMetadata>(relaxed = true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these two are not needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants