Agent Integration
gbash is designed to sit behind an agent tool boundary: one runtime, one or more persistent sessions, explicit budgets, and optional structured observability.
Go API Integration
Create a runtime, open a session, and execute scripts:
package main
import (
"context"
"fmt"
"log"
"github.com/ewhauser/gbash"
)
func main() {
ctx := context.Background()
rt, err := gbash.New()
if err != nil {
log.Fatal(err)
}
session, err := rt.NewSession(ctx)
if err != nil {
log.Fatal(err)
}
result, err := session.Exec(ctx, &gbash.ExecutionRequest{
Script: "echo hello world",
})
if err != nil {
log.Fatal(err)
}
fmt.Print(result.Stdout) // "hello world\n"
}Exposing gbash as a Tool
For most agent frameworks, return a structured tool payload instead of only stdout:
type BashToolResult struct {
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitCode int `json:"exitCode"`
FinalEnv map[string]string `json:"finalEnv,omitempty"`
}
func bashTool(ctx context.Context, session *gbash.Session, command string, env map[string]string) (*BashToolResult, error) {
result, err := session.Exec(ctx, &gbash.ExecutionRequest{
Script: command,
Env: env,
})
if err != nil {
return nil, err
}
return &BashToolResult{
Stdout: result.Stdout,
Stderr: result.Stderr,
ExitCode: result.ExitCode,
FinalEnv: result.FinalEnv,
}, nil
}That shape makes it easier for the agent host to preserve state and reason about failures without parsing free-form text.
Persistent Sessions
A Session preserves the sandbox filesystem automatically across multiple Exec calls. That is the main building block for multi-turn agent workflows:
_, _ = session.Exec(ctx, &gbash.ExecutionRequest{
Script: `echo "data" > /tmp/output.txt`,
})
result, _ := session.Exec(ctx, &gbash.ExecutionRequest{
Script: "cat /tmp/output.txt",
})
// result.Stdout == "data\n"Working directory and exported environment do not persist automatically across separate Session.Exec calls. To preserve them, feed ExecutionResult.FinalEnv into the next request:
first, _ := session.Exec(ctx, &gbash.ExecutionRequest{
Script: "cd /tmp && export MODE=debug",
})
second, _ := session.Exec(ctx, &gbash.ExecutionRequest{
Script: "pwd && echo $MODE",
Env: first.FinalEnv,
})That pattern is a good fit for agent tool loops where each turn needs to remember PWD, exported variables, or shell-derived state.
Execution Budgets Prevent Runaway Agents
LLM-generated scripts can be unpredictable. Execution budgets ensure that no single call can exhaust resources:
- Command count caps total commands per execution
- Loop iterations cap iterations per loop
- Substitution depth caps nested
$(...)and arithmetic expansion - Stdout/stderr limits truncate oversized output
- File and network limits bound individual reads and responses
When a budget is exceeded, execution stops with an error and the session remains usable for later calls.
Trace Events for Observability
If you enable tracing, execution results include structured events in result.Events. Use them to observe what the agent did without parsing stdout:
result, _ := session.Exec(ctx, &gbash.ExecutionRequest{
Script: "ls /workspace && cat /workspace/data.csv",
})
for _, event := range result.Events {
switch event.Kind {
case "command.start":
fmt.Printf("ran: %s %v\n", event.Command.Name, event.Command.Argv)
case "file.access":
fmt.Printf("read: %s\n", event.File.Path)
case "policy.denied":
fmt.Printf("blocked: %s\n", event.Policy.Subject)
}
}Trace events are particularly useful for:
- Logging agent actions for audit trails
- Detecting blocked operations without scraping stderr
- Tracking file mutations between turns
- Measuring execution complexity and tool cost
See Tracing and Logging for the available trace modes and log callbacks.
Examples
See these working examples in the repository:
- openai-tool-call - uses the OpenAI Responses API with gbash as a bash tool
- adk-bash-chat - CLI chatbot using Google ADK with a persistent gbash session
- otel - demonstrates tracing and lifecycle logging integration