Skip to content

Commit 7b59972

Browse files
wikaaaaacopybara-github
authored andcommitted
feat: add ContainerEnvironment backed by the Docker CLI
Adds an JVM Environment that runs commands and reads and writes files inside a long-lived Docker container, plus a demo agent wiring it into an EnvironmentToolset. PiperOrigin-RevId: 967183144
1 parent bc89fb2 commit 7b59972

23 files changed

Lines changed: 4635 additions & 5 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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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.agents.ReadonlyContext
20+
import com.google.adk.kt.annotations.ExperimentalEnvironmentApi
21+
import com.google.adk.kt.tools.ToolContext
22+
import kotlin.time.Duration
23+
24+
/**
25+
* The exception type wrapped in [Result.failure] by [Environment] operations.
26+
*
27+
* An environment wraps this in [Result.failure] for recoverable failures (e.g. a missing file, an
28+
* unreadable path, or a failure to launch a command). The [message] is intended to be forwarded to
29+
* the model as a tool error, so it MUST be precise and self-contained, and MUST NOT leak sensitive
30+
* internal detail. Implementations should only wrap [EnvironmentException] in [Result.failure].
31+
*/
32+
class EnvironmentException(message: String, cause: Throwable? = null) : Exception(message, cause)
33+
34+
/**
35+
* Result of a command execution.
36+
*
37+
* @property exitCode The exit code of the process.
38+
* @property stdout Standard output captured from the process.
39+
* @property stderr Standard error captured from the process.
40+
* @property timedOut Whether the execution exceeded the timeout.
41+
*/
42+
data class ExecutionResult(
43+
val exitCode: Int = 0,
44+
val stdout: String = "",
45+
val stderr: String = "",
46+
val timedOut: Boolean = false,
47+
)
48+
49+
/**
50+
* Interface for code execution environments.
51+
*
52+
* An environment provides the ability to execute shell commands, read files, and write files within
53+
* a working directory. Concrete implementations include local subprocess execution, sandboxed
54+
* execution, container environments, and cloud-hosted environments.
55+
*
56+
* The operations are `suspend` and may be invoked concurrently from parallel coroutines. This
57+
* interface does no synchronization — when implementing an environment, keep concurrency in mind:
58+
* overlapping operations can race on the shared working directory. [close] is not `suspend`, so
59+
* that an environment can be closed wherever the ADK's other [AutoCloseable] types are.
60+
*
61+
* [execute], [readFile], and [writeFile] return a [Result] whose failure case is an
62+
* [EnvironmentException] with a message intended to be surfaced to the model; implementations
63+
* should only wrap [EnvironmentException] in [Result.failure].
64+
*
65+
* Lifecycle:
66+
* 1. Construct the environment.
67+
* 2. Call [initialize] before first use.
68+
* 3. Use [execute], [readFile], [writeFile].
69+
* 4. Call [close] when done.
70+
*/
71+
@ExperimentalEnvironmentApi
72+
interface Environment : AutoCloseable {
73+
74+
/**
75+
* Initialize the environment (e.g. create the working directory).
76+
*
77+
* The default implementation is a no-op; implementations must be idempotent and safe to call
78+
* concurrently.
79+
*
80+
* @param context A readonly view of the invocation, identifying the calling session, or `null`
81+
* when no context is available (e.g. a `getTools` call that supplies none).
82+
*/
83+
suspend fun initialize(context: ReadonlyContext? = null) {}
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+
override 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 context The tool-call context, identifying the calling session.
100+
* @param command The shell command string to execute.
101+
* @param timeout Maximum execution time; `null` means no limit.
102+
* @return A [Result] wrapping an [ExecutionResult] (exit code, stdout, stderr, timeout status),
103+
* or a [Result.failure] with an [EnvironmentException] if the command could not be launched.
104+
*/
105+
suspend fun execute(
106+
context: ToolContext,
107+
command: String,
108+
timeout: Duration? = null,
109+
): Result<ExecutionResult>
110+
111+
/**
112+
* Read a file from the environment filesystem.
113+
*
114+
* @param context The tool-call context, identifying the calling session.
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(context: ToolContext, 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 context The tool-call context, identifying the calling session.
127+
* @param path Absolute or working-dir-relative path to the file.
128+
* @param content The raw bytes to write.
129+
* @return [Result.success] on success, or a [Result.failure] with an [EnvironmentException] if
130+
* the write fails.
131+
*/
132+
suspend fun writeFile(context: ToolContext, path: String, content: ByteArray): Result<Unit>
133+
}

0 commit comments

Comments
 (0)