Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/docs/python-client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ Each item in `result.results` carries `exit_code` and the buffered `stdout` from

`execute_commands()` supports the same callbacks and options as `query()`: streaming via `on_assistant_message`, artifact uploads, cancellation, timeout, and `secrets_to_redact`. Use `pre_execution_downloadables` to fetch files into the sandbox before the commands run.

Each `CommandInterface` accepts an optional `env` dict that is merged on top of the sandbox's `process.env` for that command.
Each `CommandInterface` accepts an optional `env` dict that is merged on top of the sandbox's `process.env` for that command. Set `silent=True` to suppress a command's stdout/stderr from being forwarded as `assistant_message` text_blocks -- useful for plumbing commands whose output shouldn't reach the user-facing stream. Exit codes and error semantics are unchanged.

## Cancel a Run

Expand Down
2 changes: 1 addition & 1 deletion packages/runtimeuse-client-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ except CancelledException:
| `ExecuteCommandsOptions` | Configuration for `client.execute_commands()` (callbacks, timeout) |
| `CommandExecutionResult` | Return type of `execute_commands()` (`.results`) |
| `CommandResultItem` | Per-command result (`.command`, `.exit_code`) |
| `CommandInterface` | Shell command to execute (`.command`, `.cwd`, `.env`) |
| `CommandInterface` | Shell command to execute (`.command`, `.cwd`, `.env`, `.silent`) |
| `RuntimeEnvironmentDownloadableInterface` | File to download into the runtime before invocation |

### Exceptions
Expand Down
2 changes: 1 addition & 1 deletion packages/runtimeuse-client-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "runtimeuse-client"
version = "0.13.0"
version = "0.14.0"
description = "Client library for AI agent runtime communication over WebSocket"
readme = "README.md"
license = {"text" = "FSL"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class CommandInterface(BaseModel):
cwd: str | None = None
command: str
env: dict[str, str] | None = None
# When true, the runtime will not surface this command's stdout/stderr as
# assistant_message text_blocks. Use for plumbing commands (e.g. git
# clone/push) whose output would otherwise leak into user-facing logs.
silent: bool = False


class InvocationMessage(BaseModel):
Expand Down
4 changes: 2 additions & 2 deletions packages/runtimeuse-client-python/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ async def test_sends_command_execution_message(
assert len(cmd_msgs) == 1
assert cmd_msgs[0]["source_id"] == "cmd-test"
assert cmd_msgs[0]["commands"] == [
{"command": "echo hello", "cwd": None, "env": None}
{"command": "echo hello", "cwd": None, "env": None, "silent": False}
]

@pytest.mark.asyncio
Expand Down Expand Up @@ -750,7 +750,7 @@ async def test_command_env_forwarded(
]
assert len(cmd_msgs) == 1
assert cmd_msgs[0]["commands"] == [
{"command": "echo hello", "cwd": None, "env": {"FOO": "bar"}}
{"command": "echo hello", "cwd": None, "env": {"FOO": "bar"}, "silent": False}
]

@pytest.mark.asyncio
Expand Down
2 changes: 1 addition & 1 deletion packages/runtimeuse-client-python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/runtimeuse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ The session also accepts a `command_execution_message` instead of an `invocation
Environment variables can be injected at two levels:

- **Per-command (`Command.env`)** -- each command in `pre_agent_invocation_commands`, `post_agent_invocation_commands`, or `command_execution_message.commands` can carry its own `env` map. These are merged on top of `process.env` when the command is spawned.
- **Per-command (`Command.silent`)** -- set `silent: true` to suppress a command's stdout/stderr from being forwarded as `assistant_message` text_blocks. Use it for plumbing commands whose output should not appear in the user-facing message stream. Exit codes and error semantics are unchanged.
- **Per-invocation (`InvocationMessage.agent_env`)** -- environment variables passed to the agent handler. The Claude handler merges these on top of `process.env` when calling the Claude Agent SDK. Custom handlers receive these via `AgentInvocation.env`.

## Artifact Management
Expand Down
4 changes: 2 additions & 2 deletions packages/runtimeuse/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/runtimeuse/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "runtimeuse",
"version": "0.13.0",
"version": "0.14.0",
"description": "AI agent runtime with WebSocket protocol, artifact handling, and secret management",
"license": "FSL",
"type": "module",
Expand Down
38 changes: 38 additions & 0 deletions packages/runtimeuse/src/invocation-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,26 @@ describe("InvocationRunner", () => {
});
});

it("does not forward stdout/stderr as assistant messages when command is silent", async () => {
mockExecute.mockImplementation(async (options) => {
expect(options.onStdout).toBeUndefined();
expect(options.onStderr).toBeUndefined();
return { exitCode: 0 };
});

const { runner, message, send } = createRunner({
pre_agent_invocation_commands: [
{ command: "echo hidden", cwd: "/app", silent: true },
],
});

await runner.run(message);

expect(send).not.toHaveBeenCalledWith(
expect.objectContaining({ message_type: "assistant_message" }),
);
});

it("throws when pre-agent command exits non-zero; caller decides wire error", async () => {
mockExecute.mockResolvedValueOnce({ exitCode: 2 });
const { runner, message, send, logger } = createRunner({
Expand Down Expand Up @@ -478,6 +498,24 @@ describe("InvocationRunner.runCommandsOnly", () => {
});
});

it("does not forward stdout/stderr as assistant messages when command is silent", async () => {
mockExecute.mockImplementation(async (options) => {
expect(options.onStdout).toBeUndefined();
expect(options.onStderr).toBeUndefined();
return { exitCode: 0 };
});

const { runner, message, send } = createCommandRunner({
commands: [{ command: "echo hidden", cwd: "/app", silent: true }],
});

await runner.runCommandsOnly(message);

expect(send).not.toHaveBeenCalledWith(
expect.objectContaining({ message_type: "assistant_message" }),
);
});

it("returns result with exit code when command exits non-zero", async () => {
mockExecute.mockResolvedValueOnce({ exitCode: 2 });
const { runner, message, send } = createCommandRunner();
Expand Down
30 changes: 22 additions & 8 deletions packages/runtimeuse/src/invocation-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,22 @@ export class InvocationRunner {
`Executing command: ${command.command} in directory: ${command.cwd}`,
);

const onStdout = command.silent
? undefined
: (stdout: string) =>
send({ message_type: "assistant_message", text_blocks: [stdout] });
const onStderr = command.silent
? undefined
: (stderr: string) =>
send({ message_type: "assistant_message", text_blocks: [stderr] });

const handler = new CommandHandler({
command,
secrets,
logger,
abortController,
onStdout: (stdout) =>
send({ message_type: "assistant_message", text_blocks: [stdout] }),
onStderr: (stderr) =>
send({ message_type: "assistant_message", text_blocks: [stderr] }),
onStdout,
onStderr,
});

const result = await handler.execute();
Expand Down Expand Up @@ -166,15 +173,22 @@ export class InvocationRunner {
`Executing ${phase} command: ${command.command} in directory: ${command.cwd}`,
);

const onStdout = command.silent
? undefined
: (stdout: string) =>
send({ message_type: "assistant_message", text_blocks: [stdout] });
const onStderr = command.silent
? undefined
: (stderr: string) =>
send({ message_type: "assistant_message", text_blocks: [stderr] });

const handler = new CommandHandler({
command,
secrets,
logger,
abortController,
onStdout: (stdout) =>
send({ message_type: "assistant_message", text_blocks: [stdout] }),
onStderr: (stderr) =>
send({ message_type: "assistant_message", text_blocks: [stderr] }),
onStdout,
onStderr,
});

const result = await handler.execute();
Expand Down
4 changes: 4 additions & 0 deletions packages/runtimeuse/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ interface Command {
command: string;
cwd?: string;
env?: Record<string, string>;
// When true, the runner will not surface this command's stdout/stderr as
// assistant_message text_blocks. Use for plumbing commands whose output
// would otherwise leak into user-facing logs.
silent?: boolean;
}

interface RuntimeEnvironmentDownloadable {
Expand Down