Detailed changes
@@ -36,6 +36,9 @@ type BashTool struct {
WorkingDir *MutableWorkingDir
// LLMProvider provides access to LLM services for tool validation
LLMProvider LLMServiceProvider
+ // ConversationID is the ID of the conversation this tool belongs to.
+ // It is exposed to invoked commands via SHELLEY_CONVERSATION_ID.
+ ConversationID string
}
const (
@@ -256,6 +259,9 @@ func (b *BashTool) makeBashCommand(ctx context.Context, command string, out io.W
})
env = append(env, "SKETCH=1") // signal that this has been run by Sketch, sometimes useful for scripts
env = append(env, "EDITOR=/bin/false") // interactive editors won't work
+ if b.ConversationID != "" {
+ env = append(env, "SHELLEY_CONVERSATION_ID="+b.ConversationID)
+ }
cmd.Env = env
return cmd
}
@@ -223,6 +223,45 @@ func TestExecuteBash(t *testing.T) {
}
})
+ // Test SHELLEY_CONVERSATION_ID environment variable is set when configured
+ t.Run("SHELLEY_CONVERSATION_ID Environment Variable", func(t *testing.T) {
+ bashWithConvID := &BashTool{
+ WorkingDir: NewMutableWorkingDir("/"),
+ ConversationID: "test-conv-123",
+ }
+ req := bashInput{
+ Command: "echo $SHELLEY_CONVERSATION_ID",
+ }
+
+ output, err := bashWithConvID.executeBash(ctx, req, 5*time.Second)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ want := "test-conv-123\n"
+ if output != want {
+ t.Errorf("Expected SHELLEY_CONVERSATION_ID=test-conv-123, got %q", output)
+ }
+ })
+
+ // Test SHELLEY_CONVERSATION_ID is not set when not configured
+ t.Run("SHELLEY_CONVERSATION_ID Not Set When Empty", func(t *testing.T) {
+ req := bashInput{
+ Command: "echo \"conv_id:$SHELLEY_CONVERSATION_ID:\"",
+ }
+
+ output, err := bashTool.executeBash(ctx, req, 5*time.Second)
+ if err != nil {
+ t.Fatalf("Unexpected error: %v", err)
+ }
+
+ // Should be empty since ConversationID is not set on bashTool
+ want := "conv_id::\n"
+ if output != want {
+ t.Errorf("Expected empty SHELLEY_CONVERSATION_ID, got %q", output)
+ }
+ })
+
// Test command with output to stderr
t.Run("Command with stderr", func(t *testing.T) {
req := bashInput{
@@ -57,6 +57,9 @@ type ToolSetConfig struct {
SubagentDB SubagentDB
// ParentConversationID is the ID of the parent conversation (for subagent tool).
ParentConversationID string
+ // ConversationID is the ID of the conversation these tools belong to.
+ // This is exposed to bash commands via the SHELLEY_CONVERSATION_ID environment variable.
+ ConversationID string
}
// ToolSet holds a set of tools for a single conversation.
@@ -102,6 +105,7 @@ func NewToolSet(ctx context.Context, cfg ToolSetConfig) *ToolSet {
WorkingDir: wd,
LLMProvider: cfg.LLMProvider,
EnableJITInstall: cfg.EnableJITInstall,
+ ConversationID: cfg.ConversationID,
}
// Use simplified patch schema for weaker models, full schema for sonnet/opus
@@ -363,6 +363,7 @@ func (cm *ConversationManager) ensureLoop(service llm.Service, modelID string) e
// Create tools for this conversation with the conversation's working directory
toolSetConfig.WorkingDir = cwd
toolSetConfig.ModelID = modelID
+ toolSetConfig.ConversationID = conversationID
toolSetConfig.ParentConversationID = conversationID // For subagent tool
toolSetConfig.OnWorkingDirChange = func(newDir string) {
// Persist working directory change to database