Skip to content

Commit b5c1a3e

Browse files
wikaaaaacopybara-github
authored andcommitted
feat: run skill scripts in an Environment via SkillToolset
PiperOrigin-RevId: 955297384
1 parent aef7c96 commit b5c1a3e

20 files changed

Lines changed: 2846 additions & 4 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.kt.annotations
18+
19+
/**
20+
* Marks ADK's **environment** APIs (execution environments and the environment toolset) as
21+
* experimental: their shape and semantics may change in future releases without prior notice.
22+
*
23+
* Opt in explicitly with `@OptIn(ExperimentalEnvironmentApi::class)` to use them.
24+
*/
25+
@MustBeDocumented
26+
@Retention(AnnotationRetention.BINARY)
27+
@RequiresOptIn(
28+
level = RequiresOptIn.Level.ERROR,
29+
message =
30+
"ADK environment APIs are experimental and may change at any time. " +
31+
"Opt in with @OptIn(ExperimentalEnvironmentApi::class) to acknowledge the risk.",
32+
)
33+
annotation class ExperimentalEnvironmentApi
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.kt.environment
18+
19+
import com.google.adk.kt.annotations.ExperimentalEnvironmentApi
20+
import kotlin.time.Duration
21+
22+
/**
23+
* The exception type wrapped in [Result.failure] by [Environment] operations.
24+
*
25+
* An environment wraps this in [Result.failure] for recoverable failures (e.g. a missing file, an
26+
* unreadable path, or a failure to launch a command). The [message] is intended to be forwarded to
27+
* the model as a tool error, so it MUST be precise and self-contained, and MUST NOT leak sensitive
28+
* internal detail. Implementations should only wrap [EnvironmentException] in [Result.failure].
29+
*/
30+
class EnvironmentException(message: String, cause: Throwable? = null) : Exception(message, cause)
31+
32+
/**
33+
* Result of a command execution.
34+
*
35+
* @property exitCode The exit code of the process.
36+
* @property stdout Standard output captured from the process.
37+
* @property stderr Standard error captured from the process.
38+
* @property timedOut Whether the execution exceeded the timeout.
39+
*/
40+
data class ExecutionResult(
41+
val exitCode: Int = 0,
42+
val stdout: String = "",
43+
val stderr: String = "",
44+
val timedOut: Boolean = false,
45+
) {
46+
/** Whether the command completed on its own with a zero exit code. */
47+
val isSuccess: Boolean
48+
get() = exitCode == 0 && !timedOut
49+
}
50+
51+
/**
52+
* Interface for code execution environments.
53+
*
54+
* An environment provides the ability to execute shell commands, read files, and write files within
55+
* a working directory. Concrete implementations include local subprocess execution, sandboxed
56+
* execution, container environments, and cloud-hosted environments.
57+
*
58+
* The operations are `suspend` and may be invoked concurrently from parallel coroutines. This
59+
* interface does no synchronization — when implementing an environment, keep concurrency in mind:
60+
* overlapping operations can race on the shared working directory. [close] is not `suspend`, so
61+
* that an environment can be closed wherever the ADK's other [AutoCloseable] types are.
62+
*
63+
* [execute], [readFile], and [writeFile] return a [Result] whose failure case is an
64+
* [EnvironmentException] with a message intended to be surfaced to the model; implementations
65+
* should only wrap [EnvironmentException] in [Result.failure].
66+
*
67+
* Lifecycle:
68+
* 1. Construct the environment.
69+
* 2. Call [initialize] before first use.
70+
* 3. Use [execute], [readFile], [writeFile].
71+
* 4. Call [close] when done.
72+
*/
73+
@ExperimentalEnvironmentApi
74+
interface Environment : AutoCloseable {
75+
/** The absolute path to the environment's working directory. */
76+
val workingDir: String
77+
78+
/**
79+
* Initialize the environment (e.g. create the working directory).
80+
*
81+
* Called before first use. The default implementation is a no-op; implementations should ensure
82+
* this method is idempotent.
83+
*/
84+
suspend fun initialize() {}
85+
86+
/**
87+
* Release resources held by the environment.
88+
*
89+
* Called when the environment is no longer needed. The default implementation is a no-op;
90+
* implementations should ensure this method is idempotent.
91+
*
92+
* This method is not `suspend`, so that an environment can be closed from the same places as the
93+
* rest of the ADK's [AutoCloseable] types, such as a [com.google.adk.kt.tools.Toolset]. An
94+
* implementation whose teardown is asynchronous either bridges to it here or, if waiting is not
95+
* required, starts it and returns.
96+
*/
97+
override fun close() {}
98+
99+
/**
100+
* Execute a shell command in the working directory.
101+
*
102+
* Ordinary process outcomes (non-zero exit, timeout) are returned in the [ExecutionResult]; a
103+
* failure to launch the command is a [Result.failure] wrapping an [EnvironmentException].
104+
*
105+
* @param command The shell command string to execute.
106+
* @param timeout Maximum execution time; `null` means no limit.
107+
* @return A [Result] wrapping an [ExecutionResult] (exit code, stdout, stderr, timeout status),
108+
* or a [Result.failure] with an [EnvironmentException] if the command could not be launched.
109+
*/
110+
suspend fun execute(command: String, timeout: Duration? = null): Result<ExecutionResult>
111+
112+
/**
113+
* Read a file from the environment filesystem.
114+
*
115+
* @param path Absolute or working-dir-relative path to the file.
116+
* @return A [Result] wrapping the raw file contents, or a [Result.failure] with an
117+
* [EnvironmentException] if the file does not exist or cannot be read.
118+
*/
119+
suspend fun readFile(path: String): Result<ByteArray>
120+
121+
/**
122+
* Write content to a file in the environment's filesystem.
123+
*
124+
* Parent directories are created automatically if they do not exist.
125+
*
126+
* @param path Absolute or working-dir-relative path to the file.
127+
* @param content The raw bytes to write.
128+
* @return [Result.success] on success, or a [Result.failure] with an [EnvironmentException] if
129+
* the write fails.
130+
*/
131+
suspend fun writeFile(path: String, content: ByteArray): Result<Unit>
132+
}

0 commit comments

Comments
 (0)