persistInstructionFiles fetches AGENTS.md instruction files and skills from the workspace agent, persisting both as message parts. This is called once when a workspace is first attached to a chat (or when the agent changes). Returns the formatted instruction string and skill index for injection into
( ctx context.Context, chat database.Chat, modelConfigID uuid.UUID, getWorkspaceAgent func(context.Context) (database.WorkspaceAgent, error), getWorkspaceConn func(context.Context) (workspacesdk.AgentConn, error), )
| 9019 | // instruction string and skill index for injection into the |
| 9020 | // current turn's prompt. |
| 9021 | func (p *Server) persistInstructionFiles( |
| 9022 | ctx context.Context, |
| 9023 | chat database.Chat, |
| 9024 | modelConfigID uuid.UUID, |
| 9025 | getWorkspaceAgent func(context.Context) (database.WorkspaceAgent, error), |
| 9026 | getWorkspaceConn func(context.Context) (workspacesdk.AgentConn, error), |
| 9027 | ) (instruction string, skills []chattool.SkillMeta, err error) { |
| 9028 | agent, agentParts, discoveredSkills, workspaceConnOK := p.fetchWorkspaceContext( |
| 9029 | ctx, chat, getWorkspaceAgent, getWorkspaceConn, |
| 9030 | ) |
| 9031 | // Defensive guard: fetchWorkspaceContext returns nil when the |
| 9032 | // chat has no valid workspace or the agent lookup fails. It's |
| 9033 | // cheaper to guard here than push the precondition up to all |
| 9034 | // callers. |
| 9035 | if agent == nil { |
| 9036 | return "", nil, nil |
| 9037 | } |
| 9038 | |
| 9039 | agentID := uuid.NullUUID{UUID: agent.ID, Valid: true} |
| 9040 | hasContent := false |
| 9041 | hasContextFilePart := false |
| 9042 | for _, part := range agentParts { |
| 9043 | if part.Type == codersdk.ChatMessagePartTypeContextFile { |
| 9044 | hasContextFilePart = true |
| 9045 | if part.ContextFileContent != "" { |
| 9046 | hasContent = true |
| 9047 | } |
| 9048 | } |
| 9049 | } |
| 9050 | directory := agent.ExpandedDirectory |
| 9051 | if directory == "" { |
| 9052 | directory = agent.Directory |
| 9053 | } |
| 9054 | |
| 9055 | if !hasContent { |
| 9056 | if !workspaceConnOK { |
| 9057 | return "", nil, nil |
| 9058 | } |
| 9059 | // Persist a blank context-file marker (plus any skill-only |
| 9060 | // parts) so subsequent turns skip the workspace agent dial. |
| 9061 | if !hasContextFilePart { |
| 9062 | agentParts = append([]codersdk.ChatMessagePart{{ |
| 9063 | Type: codersdk.ChatMessagePartTypeContextFile, |
| 9064 | ContextFileAgentID: agentID, |
| 9065 | }}, agentParts...) |
| 9066 | } |
| 9067 | content, err := chatprompt.MarshalParts(agentParts) |
| 9068 | if err != nil { |
| 9069 | return "", nil, nil |
| 9070 | } |
| 9071 | contextAPIKeyID, _ := aibridge.DelegatedAPIKeyIDFromContext(ctx) |
| 9072 | msgParams := database.InsertChatMessagesParams{ //nolint:exhaustruct // Fields populated by appendUserChatMessage. |
| 9073 | ChatID: chat.ID, |
| 9074 | } |
| 9075 | appendUserChatMessage(&msgParams, newUserChatMessage( |
| 9076 | contextAPIKeyID, |
| 9077 | content, |
| 9078 | database.ChatMessageVisibilityBoth, |