fix(coderd): avoid redundant workspace setup (#25615)

GPT-class chat turns could eagerly create workspaces or repeat setup
such as cloning an existing repo because the system prompt framed setup
work as the default path.

This updates chatd prompt guidance and the `create_workspace` tool
description so agents reuse existing chat and workspace context, treat
injected workspace context as already read, avoid recloning present
repositories, and create or start workspaces only when workspace-backed
work is required. Delegated chats now report workspace needs to the
parent instead of trying to create one.

> Mux opened this PR on behalf of Mike.
This commit is contained in:
Michael Suchacz
2026-05-22 16:08:07 +02:00
committed by GitHub
parent 8d0a73f0b1
commit de6d62815e
8 changed files with 87 additions and 11 deletions
+2 -4
View File
@@ -1582,11 +1582,9 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
}
userPrompt := SanitizePromptText(opts.SystemPrompt)
var workspaceAwareness string
workspaceAwareness := workspaceDetachedAwareness
if opts.WorkspaceID.Valid {
workspaceAwareness = "This chat is attached to a workspace. You can use workspace tools like execute, read_file, write_file, etc."
} else {
workspaceAwareness = "There is no workspace associated with this chat yet. Create one using the create_workspace tool before using workspace tools like execute, read_file, write_file, etc."
workspaceAwareness = workspaceAttachedAwareness
}
workspaceAwarenessContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
codersdk.ChatMessageText(workspaceAwareness),
+5 -1
View File
@@ -2270,7 +2270,7 @@ func TestCreateChatInsertsWorkspaceAwarenessMessage(t *testing.T) {
for _, msg := range messages {
if msg.Role == database.ChatMessageRoleSystem {
content := string(msg.Content.RawMessage)
if strings.Contains(content, "no workspace associated") {
if strings.Contains(content, "No workspace is attached to this chat yet") {
workspaceMsg = &msg
break
}
@@ -2279,6 +2279,10 @@ func TestCreateChatInsertsWorkspaceAwarenessMessage(t *testing.T) {
require.NotNil(t, workspaceMsg, "workspace awareness system message should exist")
require.Equal(t, database.ChatMessageRoleSystem, workspaceMsg.Role)
require.Equal(t, database.ChatMessageVisibilityModel, workspaceMsg.Visibility)
workspaceContent := string(workspaceMsg.Content.RawMessage)
require.Contains(t, workspaceContent, "Do not create or start a workspace by default")
require.Contains(t, workspaceContent, "Only call create_workspace or start_workspace")
require.NotContains(t, workspaceContent, "Create one using the create_workspace tool before using workspace tools")
})
}
+5 -1
View File
@@ -89,7 +89,11 @@ type createWorkspaceArgs struct {
func CreateWorkspace(db database.Store, organizationID, chatID uuid.UUID, options CreateWorkspaceOptions) fantasy.AgentTool {
return fantasy.NewAgentTool(
"create_workspace",
"Create a new workspace from a template. Requires a "+
"Create a new workspace from a template only when workspace-backed "+
"file inspection, command execution, or file editing is required, "+
"or when the user explicitly asks for one. Do not use this as a "+
"default first step for requests answerable from conversation "+
"context, provider tools, or external MCP tools. Requires a "+
"template_id (from list_templates). Optionally provide "+
"a name and parameter values (from read_template). "+
"If no name is given, one will be generated. "+
@@ -34,6 +34,19 @@ func newCreateWorkspaceMockStore(ctrl *gomock.Controller) *dbmock.MockStore {
return db
}
func TestCreateWorkspaceDescriptionDelaysWorkspaceCreation(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
db := newCreateWorkspaceMockStore(ctrl)
tool := CreateWorkspace(db, uuid.New(), uuid.New(), CreateWorkspaceOptions{})
info := tool.Info()
require.Contains(t, info.Description, "Create a new workspace from a template only when workspace-backed")
require.Contains(t, info.Description, "user explicitly asks")
require.Contains(t, info.Description, "Do not use this as a default first step")
}
func TestWaitForAgentReady(t *testing.T) {
t.Parallel()
@@ -135,6 +135,42 @@ func TestDefaultSystemPromptContainsVersionControlSafety(t *testing.T) {
require.Contains(t, DefaultSystemPrompt, "Never treat the original request as confirmation")
}
func TestWorkspaceAwarenessDelaysWorkspaceCreation(t *testing.T) {
t.Parallel()
detached := workspaceDetachedAwareness
require.Contains(t, detached, "No workspace is attached to this chat yet")
require.Contains(t, detached, "Do not create or start a workspace by default")
require.Contains(t, detached, "Only call create_workspace or start_workspace")
require.NotContains(t, detached, "Create one using the create_workspace tool before using workspace tools")
delegated := workspaceDetachedNoCreateAwareness
require.Contains(t, delegated, "This delegated chat cannot create or start a workspace")
require.Contains(t, delegated, "report that need to the parent agent")
require.NotContains(t, delegated, "Only call create_workspace or start_workspace")
attached := workspaceAttachedAwareness
require.Contains(t, attached, "This chat is attached to a workspace")
}
func TestDefaultSystemPromptDelaysWorkspaceCreation(t *testing.T) {
t.Parallel()
require.Contains(t, DefaultSystemPrompt, "Do not create a workspace by default")
require.Contains(t, DefaultSystemPrompt, "Do not clone repositories already present")
require.Contains(t, DefaultSystemPrompt, "including AGENTS.md")
require.NotContains(t, DefaultSystemPrompt, "create and start one first using create_workspace and start_workspace")
}
func TestPlanningOverlayPromptDelaysWorkspaceCreation(t *testing.T) {
t.Parallel()
prompt := PlanningOverlayPrompt()
require.Contains(t, prompt, "do not create one as the first action merely because you are planning")
require.Contains(t, prompt, "Before cloning, inspect the current workspace and reuse existing repositories")
require.NotContains(t, prompt, "create and start one with create_workspace and start_workspace before investigating")
}
func TestInsertSystemInstructionAfterSystemMessages(t *testing.T) {
t.Parallel()
+20 -2
View File
@@ -2,6 +2,17 @@ package chatd
const defaultSystemPromptPlanPathBlockPlaceholder = "{{CODER_CHAT_PLAN_FILE_PATH_BLOCK}}"
const workspaceAttachedAwareness = "This chat is attached to a workspace. You can use workspace tools like execute, read_file, write_file, etc."
const workspaceDetachedAwarenessBase = `No workspace is attached to this chat yet.
Do not create or start a workspace by default. Many requests can be completed using the conversation, provider tools such as web_search when available, or configured external MCP tools.
Workspace tools such as execute, read_file, write_file, and edit_files require an attached workspace.`
const workspaceDetachedAwareness = workspaceDetachedAwarenessBase + ` Only call create_workspace or start_workspace when the user explicitly asks for a workspace-backed task, or when the task cannot be completed without inspecting, editing, or running files in a workspace.
If a workspace is needed, use list_templates and read_template as needed before create_workspace.`
const workspaceDetachedNoCreateAwareness = workspaceDetachedAwarenessBase + ` This delegated chat cannot create or start a workspace. If workspace-backed work is required, report that need to the parent agent instead of trying workspace tools.`
// DefaultSystemPrompt is used for new chats when no deployment override is
// configured.
const DefaultSystemPrompt = `You are the Coder agent — an interactive chat tool that helps users with software-engineering tasks inside of the Coder product.
@@ -15,6 +26,8 @@ You MUST execute AS MANY TOOLS to help the user accomplish their task.
You are COMFORTABLE with vague tasks - using your tools to collect the most relevant answer possible.
If a user asks how something works, no matter how vague, you MUST use your tools to collect the most relevant answer possible.
Use tools first to gather context and make progress.
When no workspace is attached, use available non-workspace tools first. Do not create a workspace by default.
Reuse existing chat and workspace context. Do not clone repositories already present in the workspace. Treat injected <workspace-context> files, including AGENTS.md, as read; re-read only for exact current contents or suspected changes.
Do not ask clarifying questions if the answer can be obtained from the codebase, workspace, or existing project conventions.
Ask concise clarifying questions only when:
- the user's intent is materially ambiguous;
@@ -96,7 +109,9 @@ Propose a plan when:
- The task is too ambiguous to implement with confidence.
- The user asks for a plan.
If no workspace is attached to this chat yet, create and start one first using create_workspace and start_workspace.
If no workspace is attached to this chat yet, do not create one as the first action merely because you are planning.
First use the conversation, provider tools such as web_search when available, configured external MCP tools, and template metadata when they are sufficient.
Create and start a workspace only when the plan requires inspecting, editing, or running workspace files, or before writing the required plan artifact if no other valid plan path is available.
Once a workspace is available:
` + defaultSystemPromptPlanningGuidance + `
2. Use write_file to create a Markdown plan file at the absolute
@@ -114,8 +129,11 @@ var planningOverlayPrompt = `You are in Plan Mode.
Every response must work toward producing a plan.
The only intentional authored workspace artifact is the plan file at the path specified in the <plan-file-path> block below.
You may use execute and process_output for exploration, including cloning repositories, searching code, and running inspection commands needed to build the plan.
Before cloning, inspect the current workspace and reuse existing repositories when they are already available.
Do not use Plan Mode to implement the requested changes or intentionally modify project files outside the plan file.
If no workspace is attached to this chat yet, create and start one with create_workspace and start_workspace before investigating.
If no workspace is attached to this chat yet, do not create one as the first action merely because you are planning.
First use the conversation, provider tools such as web_search when available, configured external MCP tools, and template metadata when they are sufficient.
Create and start a workspace only when the plan requires inspecting, editing, or running workspace files, or before writing the required plan artifact if no other valid plan path is available.
If the plan file already exists, read it first with read_file before replacing or refining it.
` + planningOverlaySubagentGuidance() + `
Use write_file to create the plan file and edit_files to refine it.
+2 -2
View File
@@ -1005,9 +1005,9 @@ func (p *Server) createChildSubagentChatWithOptions(
return xerrors.Errorf("insert child chat: %w", err)
}
workspaceAwareness := "There is no workspace associated with this chat yet. Create one using the create_workspace tool before using workspace tools like execute, read_file, write_file, etc."
workspaceAwareness := workspaceDetachedNoCreateAwareness
if insertedChat.WorkspaceID.Valid {
workspaceAwareness = "This chat is attached to a workspace. You can use workspace tools like execute, read_file, write_file, etc."
workspaceAwareness = workspaceAttachedAwareness
}
workspaceAwarenessContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
codersdk.ChatMessageText(workspaceAwareness),