Skip to content

Commit 68e7197

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

20 files changed

Lines changed: 2849 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: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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 [BaseEnvironment] 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+
* Methods are `suspend` and may be invoked concurrently from parallel coroutines. This interface
59+
* does no synchronization — when implementing an environment, keep concurrency in mind: overlapping
60+
* operations can race on the shared working directory.
61+
*
62+
* [execute], [readFile], and [writeFile] return a [Result] whose failure case is an
63+
* [EnvironmentException] with a message intended to be surfaced to the model; implementations
64+
* should only wrap [EnvironmentException] in [Result.failure].
65+
*
66+
* Lifecycle:
67+
* 1. Construct the environment.
68+
* 2. Call [initialize] before first use.
69+
* 3. Use [execute], [readFile], [writeFile].
70+
* 4. Call [close] when done.
71+
*/
72+
@ExperimentalEnvironmentApi
73+
interface BaseEnvironment {
74+
/** The absolute path to the environment's working directory. */
75+
val workingDir: String
76+
77+
/**
78+
* Initialize the environment (e.g. create the working directory).
79+
*
80+
* Called before first use. The default implementation is a no-op; implementations should ensure
81+
* this method is idempotent.
82+
*/
83+
suspend fun initialize() {}
84+
85+
/**
86+
* Release resources held by the environment.
87+
*
88+
* Called when the environment is no longer needed. The default implementation is a no-op;
89+
* implementations should ensure this method is idempotent.
90+
*/
91+
suspend fun close() {}
92+
93+
/**
94+
* Execute a shell command in the working directory.
95+
*
96+
* Ordinary process outcomes (non-zero exit, timeout) are returned in the [ExecutionResult]; a
97+
* failure to launch the command is a [Result.failure] wrapping an [EnvironmentException].
98+
*
99+
* @param command The shell command string to execute.
100+
* @param timeout Maximum execution time; `null` means no limit.
101+
* @return A [Result] wrapping an [ExecutionResult] (exit code, stdout, stderr, timeout status),
102+
* or a [Result.failure] with an [EnvironmentException] if the command could not be launched.
103+
*/
104+
suspend fun execute(command: String, timeout: Duration? = null): Result<ExecutionResult>
105+
106+
/**
107+
* Read a file from the environment filesystem.
108+
*
109+
* @param path Absolute or working-dir-relative path to the file.
110+
* @return A [Result] wrapping the raw file contents, or a [Result.failure] with an
111+
* [EnvironmentException] if the file does not exist or cannot be read.
112+
*/
113+
suspend fun readFile(path: String): Result<ByteArray>
114+
115+
/**
116+
* Write content to a file in the environment's filesystem.
117+
*
118+
* Parent directories are created automatically if they do not exist.
119+
*
120+
* @param path Absolute or working-dir-relative path to the file.
121+
* @param content The raw bytes to write.
122+
* @return [Result.success] on success, or a [Result.failure] with an [EnvironmentException] if
123+
* the write fails.
124+
*/
125+
suspend fun writeFile(path: String, content: ByteArray): Result<Unit>
126+
}

0 commit comments

Comments
 (0)